"""Train a Q-learning agent to play BabySnake, then watch it play. Run this file to train and watch: python train_babysnake.py This module wires the generic Q-learning algorithm in q_learning.py up to BabySnake specifically, using retro_gamer.GameEnvironment (configured by babysnake/pyproject.toml's observation_function) as the environment. """ import babysnake import q_learning from babysnake_env import ACTIONS, get_state from retro.input import ProgrammaticInput from retro_gamer import GameEnvironment, GameMetadata def train( episodes=1000, alpha=0.1, gamma=0.95, epsilon=1.0, epsilon_decay=0.995, epsilon_min=0.05, max_steps_per_episode=500, ): """Train a Q-learning agent on BabySnake. Arguments: episodes (int): How many episodes to run. alpha (float): Learning rate. gamma (float): Discount factor. epsilon (float): Starting exploration rate. epsilon_decay (float): Multiply epsilon by this each episode. epsilon_min (float): Epsilon never falls below this. max_steps_per_episode (int): Safety cutoff. Without this, a lucky random walk that keeps finding food (each pickup restores more energy than a turn costs) can make an episode run far longer than intended, or even effectively forever. Returns: dict: The trained Q-table. """ q_table = {} env = GameEnvironment(babysnake.create_game, GameMetadata.from_pyproject("babysnake")) for episode in range(episodes): state = env.reset() total_reward = 0.0 for _ in range(max_steps_per_episode): if not env.game.playing: break action = q_learning.choose_action(q_table, state, ACTIONS, epsilon) next_state, reward, done = env.step(action) q_learning.update_q(q_table, state, action, reward, next_state, ACTIONS, alpha, gamma) state = next_state total_reward += reward epsilon = max(epsilon_min, epsilon * epsilon_decay) if (episode + 1) % 100 == 0: print( f"Episode {episode + 1:5d} " f"reward={total_reward:6.1f} " f"score={env.game.state['score']} " f"epsilon={epsilon:.3f} " f"q_entries={len(q_table)}" ) return q_table def watch(q_table=None): """Watch the trained agent play in the terminal. Arguments: q_table (dict | None): A trained Q-table. If None, trains first. """ if q_table is None: print("Training first...") q_table = train() inp = ProgrammaticInput() class PolicyInput: """An input source that picks actions from the Q-table.""" def collect(self): state = get_state(game) action = q_learning.choose_action(q_table, state, ACTIONS, epsilon=0.0) inp.press(action) return inp.collect() game = babysnake.create_game() game.play(input_source=PolicyInput()) if __name__ == '__main__': print("Training Q-learning agent on BabySnake...") q_table = train() print(f"\nDone. Q-table has {len(q_table)} entries.") print("\nWatching trained agent (press Enter or Escape to quit)...") watch(q_table)