generated from mwc/lab_tic_tac_toe
	Initial commit
This commit is contained in:
		
							
								
								
									
										9
									
								
								.commit_template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.commit_template
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					(Commit summary. Replace this with a one-line description of this commit.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					What I changed
 | 
				
			||||||
 | 
					(Replace this with a description of what you changed in this commit. This should be 1-2 sentences.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Why I changed it
 | 
				
			||||||
 | 
					(Describe why you made these changes. Were you working toward a goal? Did you reorganize your code? This should be 1-2 sentences.)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Estimate for remaining time to finish assignment: [REPLACE WITH TIME ESTIMATE]
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					*.swo
 | 
				
			||||||
 | 
					**/__pycache__/*
 | 
				
			||||||
							
								
								
									
										34
									
								
								nim/game_stub.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								nim/game_stub.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					class NimGameStub:
 | 
				
			||||||
 | 
					    """A stub is a minimal version of a class which stands in for the 
 | 
				
			||||||
 | 
					    real class, which hasn't yet been written. The stub has all the correct 
 | 
				
			||||||
 | 
					    methods, and their inputs and outputs are the right kind of thing, 
 | 
				
			||||||
 | 
					    but it doesn't really do anything. 
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def get_initial_state(self):
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "board": [1, 3, 5, 7],
 | 
				
			||||||
 | 
					            "first_player": True
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_next_state(self, state, action):
 | 
				
			||||||
 | 
					        next_state = {
 | 
				
			||||||
 | 
					            "board": state["board"].copy(),
 | 
				
			||||||
 | 
					            "first_player": not state["first_player"],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return next_state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_actions(self, state):
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            (0, 0), 
 | 
				
			||||||
 | 
					            (1, 0), (1, 1),
 | 
				
			||||||
 | 
					            (2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2), (3, 3),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_reward(self, state):
 | 
				
			||||||
 | 
					        return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_over(self, state):
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_objective(self, state):
 | 
				
			||||||
 | 
					        return max if state["first_player"] else min
 | 
				
			||||||
							
								
								
									
										35
									
								
								nim/player.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								nim/player.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					from nim.game_stub import NimGameStub
 | 
				
			||||||
 | 
					from strategy.lookahead_strategy import LookaheadStrategy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HumanNimPlayer:
 | 
				
			||||||
 | 
					    def __init__(self, name):
 | 
				
			||||||
 | 
					        self.name = name
 | 
				
			||||||
 | 
					        self.game = NimGameStub()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state):
 | 
				
			||||||
 | 
					        actions = self.game.get_actions(state)
 | 
				
			||||||
 | 
					        for i, action in enumerate(actions):
 | 
				
			||||||
 | 
					            row, lines_to_remove = action
 | 
				
			||||||
 | 
					            print(f"{i}. Remove {lines_to_remove} from row {row}.")
 | 
				
			||||||
 | 
					        choice = self.get_int(len(actions))
 | 
				
			||||||
 | 
					        return actions[choice]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_int(self, maximum):
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            response = input("> ")
 | 
				
			||||||
 | 
					            if response.isdigit():
 | 
				
			||||||
 | 
					                value = int(response)
 | 
				
			||||||
 | 
					                if value < maximum:
 | 
				
			||||||
 | 
					                    return value
 | 
				
			||||||
 | 
					            print("Invalid input.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ComputerNimPlayer:
 | 
				
			||||||
 | 
					    def __init__(self, name):
 | 
				
			||||||
 | 
					        self.name = name
 | 
				
			||||||
 | 
					        self.strategy = LookaheadStrategy(NimGameStub(), max_depth=3, deterministic=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state):
 | 
				
			||||||
 | 
					        action = self.strategy.choose_action(state)
 | 
				
			||||||
 | 
					        row, lines_to_remove = action
 | 
				
			||||||
 | 
					        print(f"{self.name} removes {lines_to_remove} from row {row}")
 | 
				
			||||||
 | 
					        return action
 | 
				
			||||||
							
								
								
									
										32
									
								
								nim/view.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								nim/view.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					from nim.game_stub import NimGameStub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NimView:
 | 
				
			||||||
 | 
					    def __init__(self, player0, player1):
 | 
				
			||||||
 | 
					        self.players = [player0, player1]
 | 
				
			||||||
 | 
					        self.game = NimGameStub()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def greet(self):
 | 
				
			||||||
 | 
					        print(f"{self.players[0].name} and {self.players[1].name}, welcome to Nim.")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def show_board(self, state):
 | 
				
			||||||
 | 
					        for lines_in_row in state["board"]:
 | 
				
			||||||
 | 
					            print("| " * lines_in_row)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_action(self, state):
 | 
				
			||||||
 | 
					        self.show_board(state)
 | 
				
			||||||
 | 
					        player = self.get_current_player(state)
 | 
				
			||||||
 | 
					        return player.choose_action(state)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_current_player(self, state):
 | 
				
			||||||
 | 
					        if state["first_player"]:
 | 
				
			||||||
 | 
					            return self.players[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return self.players[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def conclude(self, state):
 | 
				
			||||||
 | 
					        self.show_board(state)
 | 
				
			||||||
 | 
					        if self.game.get_reward(state) > 0:
 | 
				
			||||||
 | 
					            winner = self.players[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            winner = self.players[1]
 | 
				
			||||||
 | 
					        print(f"Congratulations, {winner.name}!")
 | 
				
			||||||
							
								
								
									
										35
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					# Tic Tac Toe notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Checkpoint 1 Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Which class is responsible for each of the following behaviors?
 | 
				
			||||||
 | 
					For each, explain how the behavior is accomplished.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Checking to see whether the game is over
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Determining which actions are available at a particular state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Showing the board
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Choosing which action to play on a turn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Checkpoint 2 Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### TTT Strategy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For each of the following board states, if you are playing as X
 | 
				
			||||||
 | 
					and it's your turn, which action would you take? Why?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   | O | O       |   | O       | X |       X | O |   
 | 
				
			||||||
 | 
					---+---+---   ---+---+---   ---+---+---   ---+---+---
 | 
				
			||||||
 | 
					 X | X |         | X |       X | O | O       |   |   
 | 
				
			||||||
 | 
					---+---+---   ---+---+---   ---+---+---   ---+---+---
 | 
				
			||||||
 | 
					   |   |         |   | O       |   |         |   |   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Initial game state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can get the inital game state using game.get_initial_state(). 
 | 
				
			||||||
 | 
					What is the current and future reward for this state? What does this mean? 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								play_nim.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								play_nim.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					from nim.game_stub import NimGameStub
 | 
				
			||||||
 | 
					from nim.view import NimView
 | 
				
			||||||
 | 
					from nim.player import HumanNimPlayer, ComputerNimPlayer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					player0 = HumanNimPlayer(input("What's your name? "))
 | 
				
			||||||
 | 
					player1 = ComputerNimPlayer("Robot")
 | 
				
			||||||
 | 
					view = NimView(player0, player1)
 | 
				
			||||||
 | 
					game = NimGameStub()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					view.greet()
 | 
				
			||||||
 | 
					state = game.get_initial_state()
 | 
				
			||||||
 | 
					while not game.is_over(state):
 | 
				
			||||||
 | 
					    action = view.get_action(state)
 | 
				
			||||||
 | 
					    state = game.get_next_state(state, action)
 | 
				
			||||||
 | 
					view.conclude(state)
 | 
				
			||||||
							
								
								
									
										15
									
								
								play_ttt.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								play_ttt.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					from ttt.game import TTTGame
 | 
				
			||||||
 | 
					from ttt.view import TTTView
 | 
				
			||||||
 | 
					from ttt.player import TTTHumanPlayer, TTTComputerPlayer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					player0 = TTTHumanPlayer("Player 1")
 | 
				
			||||||
 | 
					player1 = TTTHumanPlayer("Player 2")
 | 
				
			||||||
 | 
					game = TTTGame()
 | 
				
			||||||
 | 
					view = TTTView(player0, player1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					state = game.get_initial_state()
 | 
				
			||||||
 | 
					view.greet()
 | 
				
			||||||
 | 
					while not game.is_over(state):
 | 
				
			||||||
 | 
					    action = view.get_action(state)
 | 
				
			||||||
 | 
					    state = game.get_next_state(state, action)
 | 
				
			||||||
 | 
					view.conclude(state)
 | 
				
			||||||
							
								
								
									
										33
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								poetry.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "click"
 | 
				
			||||||
 | 
					version = "8.1.3"
 | 
				
			||||||
 | 
					description = "Composable command line interface toolkit"
 | 
				
			||||||
 | 
					category = "main"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = ">=3.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
 | 
				
			||||||
 | 
					    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[package.dependencies]
 | 
				
			||||||
 | 
					colorama = {version = "*", markers = "platform_system == \"Windows\""}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "colorama"
 | 
				
			||||||
 | 
					version = "0.4.6"
 | 
				
			||||||
 | 
					description = "Cross-platform colored terminal text."
 | 
				
			||||||
 | 
					category = "main"
 | 
				
			||||||
 | 
					optional = false
 | 
				
			||||||
 | 
					python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 | 
				
			||||||
 | 
					files = [
 | 
				
			||||||
 | 
					    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
 | 
				
			||||||
 | 
					    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[metadata]
 | 
				
			||||||
 | 
					lock-version = "2.0"
 | 
				
			||||||
 | 
					python-versions = "^3.11"
 | 
				
			||||||
 | 
					content-hash = "101b8706a8befcaae12f34f371e35e5bc371645d8ce2747a4b32cca44ff8e832"
 | 
				
			||||||
							
								
								
									
										16
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					[tool.poetry]
 | 
				
			||||||
 | 
					name = "lab_tic_tac_toe"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					description = ""
 | 
				
			||||||
 | 
					authors = ["Chris Proctor <chris@chrisproctor.net>"]
 | 
				
			||||||
 | 
					license = "MIT"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.poetry.dependencies]
 | 
				
			||||||
 | 
					python = "^3.11"
 | 
				
			||||||
 | 
					click = "^8.1.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.poetry.dev-dependencies]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build-system]
 | 
				
			||||||
 | 
					requires = ["poetry-core>=1.0.0"]
 | 
				
			||||||
 | 
					build-backend = "poetry.core.masonry.api"
 | 
				
			||||||
							
								
								
									
										96
									
								
								strategy/lookahead_strategy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								strategy/lookahead_strategy.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					from types import MethodType
 | 
				
			||||||
 | 
					from random import choice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LookaheadStrategy:
 | 
				
			||||||
 | 
					    """A Strategy which considers the future consequences of an action.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    To initialize a LookaheadStrategy, pass in an instance of a game containing
 | 
				
			||||||
 | 
					    the following methods. These methods encode the rules of the game, 
 | 
				
			||||||
 | 
					    which a LookaheadStrategy needs to know in order to determine which move is best.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - get_next_state: state, action -> state
 | 
				
			||||||
 | 
					        - get_actions: state -> [actions]
 | 
				
			||||||
 | 
					        - get_reward: state -> int
 | 
				
			||||||
 | 
					        - is_over: state -> bool
 | 
				
			||||||
 | 
					        - get_objective: str -> function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Optionally, pass the following arguments to control the behavior of the LookaheadStrategy:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - max_depth: int. A game may be too complex to search the full state tree.
 | 
				
			||||||
 | 
					          Setting max_depth will set a cutoff on how far ahead the LookaheadStrategy will look.
 | 
				
			||||||
 | 
					        - deterministic: bool. It's possible that there are multiple equally-good actions. 
 | 
				
			||||||
 | 
					          When deterministic is True, LookaheadStrategy will always choose the first of the 
 | 
				
			||||||
 | 
					          equally-good actions, so that LookaheadStrategy will always play out the same game. 
 | 
				
			||||||
 | 
					          When deterministic is False, LookaheadStrategy will choose randomly from all actions
 | 
				
			||||||
 | 
					          which are equally-good. 
 | 
				
			||||||
 | 
					        - Explain: When set to True, LookaheadStrategy will print out its reasoning. 
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, game, max_depth=None, deterministic=True, explain=False):
 | 
				
			||||||
 | 
					        self.validate_game(game)
 | 
				
			||||||
 | 
					        self.game = game
 | 
				
			||||||
 | 
					        self.max_depth = max_depth
 | 
				
			||||||
 | 
					        self.deterministic = deterministic
 | 
				
			||||||
 | 
					        self.explain = explain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state, depth=0):
 | 
				
			||||||
 | 
					        """Given a state, chooses an action.
 | 
				
			||||||
 | 
					        This is the most important method of a Strategy, corresponding to the situation where
 | 
				
			||||||
 | 
					        it's a player's turn to play a game and she needs to decide what to do. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Strategy chooses an action by considering all possible actions, and finding the 
 | 
				
			||||||
 | 
					        total current and future reward which would come from playing that action. 
 | 
				
			||||||
 | 
					        Then we use the game's objective to choose the "best" reward. Usually bigger is better, 
 | 
				
			||||||
 | 
					        but in zero-sum games like tic tac toe, the players want opposite outcomes. One player
 | 
				
			||||||
 | 
					        wants the reward to be high, while the other wants the reward to be low.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Once we know which reward is best, we choose an action which will lead to that reward.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        possible_actions = self.game.get_actions(state)
 | 
				
			||||||
 | 
					        rewards = {}
 | 
				
			||||||
 | 
					        for action in possible_actions:
 | 
				
			||||||
 | 
					            future_state = self.game.get_next_state(state, action)
 | 
				
			||||||
 | 
					            rewards[action] = self.get_current_and_future_reward(future_state, depth=depth)
 | 
				
			||||||
 | 
					        objective = self.game.get_objective(state)
 | 
				
			||||||
 | 
					        best_reward = objective(rewards.values())
 | 
				
			||||||
 | 
					        best_actions = [action for action in possible_actions if rewards[action] == best_reward]
 | 
				
			||||||
 | 
					        if self.deterministic:
 | 
				
			||||||
 | 
					            action = best_actions[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            action = choice(best_actions)
 | 
				
			||||||
 | 
					        if self.explain:
 | 
				
			||||||
 | 
					            self.print_explanation(state, action, rewards[action], depth)
 | 
				
			||||||
 | 
					        return action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_current_and_future_reward(self, state, depth=0):
 | 
				
			||||||
 | 
					        """Calculates the reward from this state, and from all future states which would be 
 | 
				
			||||||
 | 
					        reached, assuming all players are using this Strategy.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        reward = self.game.get_reward(state)
 | 
				
			||||||
 | 
					        if (self.max_depth is None or depth <= self.max_depth) and not self.game.is_over(state):
 | 
				
			||||||
 | 
					            action = self.choose_action(state, depth=depth+1)
 | 
				
			||||||
 | 
					            future_state = self.game.get_next_state(state, action)
 | 
				
			||||||
 | 
					            reward += self.get_current_and_future_reward(future_state, depth=depth+1)
 | 
				
			||||||
 | 
					        return reward
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_game(self, game):
 | 
				
			||||||
 | 
					        "Checks that the game has all the required methods."
 | 
				
			||||||
 | 
					        required_methods = [
 | 
				
			||||||
 | 
					            "get_next_state",
 | 
				
			||||||
 | 
					            "get_actions",
 | 
				
			||||||
 | 
					            "get_reward",
 | 
				
			||||||
 | 
					            "is_over",
 | 
				
			||||||
 | 
					            "get_objective",
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        for method in required_methods:
 | 
				
			||||||
 | 
					            if not (hasattr(game, method) and isinstance(getattr(game, method), MethodType)):
 | 
				
			||||||
 | 
					                message = f"Game {game} does not have method {method}."
 | 
				
			||||||
 | 
					                raise ValueError(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def print_explanation(self, state, action, reward, depth):
 | 
				
			||||||
 | 
					        """Prints out the current state of exploration of the state tree"""
 | 
				
			||||||
 | 
					        indent = '│ ' * (max(0, depth-1)) + ('├ ' if depth > 0 else '')
 | 
				
			||||||
 | 
					        print(f"{indent}[{reward}] Best action: {action} {state}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								strategy/random_strategy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								strategy/random_strategy.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					from random import choice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RandomStrategy:
 | 
				
			||||||
 | 
					    """A Strategy which randomly chooses a move. Not a great choice.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def __init__(self, game):
 | 
				
			||||||
 | 
					        self.game = game
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state):
 | 
				
			||||||
 | 
					        possible_actions = self.game.get_actions(state)
 | 
				
			||||||
 | 
					        return choice(possible_actions)
 | 
				
			||||||
							
								
								
									
										61
									
								
								ttt/game.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ttt/game.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					class TTTGame:
 | 
				
			||||||
 | 
					    "Models a tic-tac-toe game."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_initial_state(self):
 | 
				
			||||||
 | 
					        "Returns the game's initial state."
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "board": ['-', '-', '-', '-', '-', '-', '-', '-', '-'],
 | 
				
			||||||
 | 
					            "player_x": True,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_next_state(self, state, action):
 | 
				
			||||||
 | 
					        """Given a state and an action, returns the resulting state.
 | 
				
			||||||
 | 
					        In the resulting state, the current player's symbol has been placed 
 | 
				
			||||||
 | 
					        in an empty board space, and it is the opposite player's turn.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        new_board = state["board"].copy()
 | 
				
			||||||
 | 
					        new_board[action] = 'X' if state["player_x"] else 'O'
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            "board": new_board,
 | 
				
			||||||
 | 
					            "player_x": not state["player_x"],
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_actions(self, state):
 | 
				
			||||||
 | 
					        "Returns a list of the indices of empty spaces"
 | 
				
			||||||
 | 
					        return [index for index in range(9) if state["board"][index] == '-']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def is_over(self, state):
 | 
				
			||||||
 | 
					        "Checks whether the game is over."
 | 
				
			||||||
 | 
					        return self.board_is_full(state) or self.check_winner(state, 'X') or self.check_winner(state, 'O')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_reward(self, state):
 | 
				
			||||||
 | 
					        """Determines the reward associated with reaching this state.
 | 
				
			||||||
 | 
					        For tic-tac-toe, the two opponents each want a different game outcome. So 
 | 
				
			||||||
 | 
					        we set the reward for X winning to 1 and the reward for O winning to -1.
 | 
				
			||||||
 | 
					        All other states (unfinished games and games which ended in a draw) are worth 0.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.check_winner(state, 'X'):
 | 
				
			||||||
 | 
					            return 1
 | 
				
			||||||
 | 
					        elif self.check_winner(state, 'O'):
 | 
				
			||||||
 | 
					            return -1
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_objective(self, state):
 | 
				
			||||||
 | 
					        """Returns a player's objective, or a function describing what a player wants. 
 | 
				
			||||||
 | 
					        This function should choose the best value from a list. In tic tac toe, the players
 | 
				
			||||||
 | 
					        want opposite things, so we set X's objective to the built-in function `max`
 | 
				
			||||||
 | 
					        (which chooses the largest number), and we set O's objective to the built-in function `min`.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return max if state["player_x"] else min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def board_is_full(self, state):
 | 
				
			||||||
 | 
					        "Checks whether all the spaces in the board are occupied."
 | 
				
			||||||
 | 
					        for space in state["board"]:
 | 
				
			||||||
 | 
					            if space == '-':
 | 
				
			||||||
 | 
					                return False
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_winner(self, state, symbol):
 | 
				
			||||||
 | 
					        "Checks whether the player with `symbol` has won the game."
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
							
								
								
									
										33
									
								
								ttt/player.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								ttt/player.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					from click import Choice, prompt
 | 
				
			||||||
 | 
					from strategy.random_strategy import RandomStrategy
 | 
				
			||||||
 | 
					from ttt.game import TTTGame
 | 
				
			||||||
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TTTHumanPlayer:
 | 
				
			||||||
 | 
					    "A human tic tac toe player."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name):
 | 
				
			||||||
 | 
					        "Sets up the player."
 | 
				
			||||||
 | 
					        self.name = name
 | 
				
			||||||
 | 
					        self.game = TTTGame()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state):
 | 
				
			||||||
 | 
					        "Chooses an action by prompting the player for a choice."
 | 
				
			||||||
 | 
					        actions = self.game.get_actions(state)
 | 
				
			||||||
 | 
					        choices = Choice([str(action) for action in actions])
 | 
				
			||||||
 | 
					        action = int(prompt("> ", type=choices, show_choices=False))
 | 
				
			||||||
 | 
					        return action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TTTComputerPlayer:
 | 
				
			||||||
 | 
					    "A computer tic tac toe player"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, name):
 | 
				
			||||||
 | 
					        "Sets up the player."
 | 
				
			||||||
 | 
					        self.name = name
 | 
				
			||||||
 | 
					        self.strategy = RandomStrategy(TTTGame())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def choose_action(self, state):
 | 
				
			||||||
 | 
					        "Chooses a random move from the moves available."
 | 
				
			||||||
 | 
					        action = self.strategy.choose_action(state)
 | 
				
			||||||
 | 
					        print(f"{self.name} chooses {action}.")
 | 
				
			||||||
 | 
					        return action
 | 
				
			||||||
							
								
								
									
										75
									
								
								ttt/view.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								ttt/view.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					from ttt.game import TTTGame
 | 
				
			||||||
 | 
					import click
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TTTView:
 | 
				
			||||||
 | 
					    "Handles user interaction with a tic-tac-toe game."
 | 
				
			||||||
 | 
					    greeting = "Welcome to tic-tac-toe"
 | 
				
			||||||
 | 
					    goodbye = "Well, that's a wrap."
 | 
				
			||||||
 | 
					    divider = "---+---+---"
 | 
				
			||||||
 | 
					    x_color = "red"
 | 
				
			||||||
 | 
					    o_color = "blue"
 | 
				
			||||||
 | 
					    option_color = "bright_black"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, playerX, playerO):
 | 
				
			||||||
 | 
					        self.game = TTTGame()
 | 
				
			||||||
 | 
					        self.players = {
 | 
				
			||||||
 | 
					            "X": playerX, 
 | 
				
			||||||
 | 
					            "O": playerO,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def greet(self):
 | 
				
			||||||
 | 
					        "Starts a new game by greeting the players."
 | 
				
			||||||
 | 
					        x_name = self.players['X'].name
 | 
				
			||||||
 | 
					        o_name = self.players['O'].name
 | 
				
			||||||
 | 
					        print(self.greeting)
 | 
				
			||||||
 | 
					        print(f"{x_name} will play as X.")
 | 
				
			||||||
 | 
					        print(f"{o_name} will play as O.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_action(self, state):
 | 
				
			||||||
 | 
					        "Shows the board and asks the current player for their choice of action."
 | 
				
			||||||
 | 
					        self.print_board(state)
 | 
				
			||||||
 | 
					        current_player_symbol = 'X' if state["player_x"] else 'O'
 | 
				
			||||||
 | 
					        player = self.players[current_player_symbol]
 | 
				
			||||||
 | 
					        print(f"{player.name}, it's your move.")
 | 
				
			||||||
 | 
					        return player.choose_action(state)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def print_board(self, state):
 | 
				
			||||||
 | 
					        "Prints the current board, showing indices of available spaces"
 | 
				
			||||||
 | 
					        print(self.format_row(state, [0, 1, 2]))
 | 
				
			||||||
 | 
					        print(self.divider)
 | 
				
			||||||
 | 
					        print(self.format_row(state, [3, 4, 5]))
 | 
				
			||||||
 | 
					        print(self.divider)
 | 
				
			||||||
 | 
					        print(self.format_row(state, [6, 7, 8]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def format_row(self, state, indices):
 | 
				
			||||||
 | 
					        "Returns a string for one row in the board, like ' X | O | X '"
 | 
				
			||||||
 | 
					        spaces = [self.format_value(state, i) for i in indices]
 | 
				
			||||||
 | 
					        return f" {spaces[0]} | {spaces[1]} | {spaces[2]} "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def format_value(self, state, index):
 | 
				
			||||||
 | 
					        """Formats the value for a single space on the board. 
 | 
				
			||||||
 | 
					        If the game board already has a symbol in that space, formats that value for the Terminal.
 | 
				
			||||||
 | 
					        If the space is empty, instead formats the index of the space. 
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if state["board"][index] == 'X':
 | 
				
			||||||
 | 
					            return click.style('X', fg=self.x_color)
 | 
				
			||||||
 | 
					        elif state["board"][index] == 'O':
 | 
				
			||||||
 | 
					            return click.style('O', fg=self.o_color)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return click.style(index, fg=self.option_color)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def conclude(self, state):
 | 
				
			||||||
 | 
					        """Says goodbye.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.print_board(state)
 | 
				
			||||||
 | 
					        if self.game.check_winner(state, 'X'):
 | 
				
			||||||
 | 
					            winner = self.players['X']
 | 
				
			||||||
 | 
					        elif self.game.check_winner(state, 'O'):
 | 
				
			||||||
 | 
					            winner = self.players['O']
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            winner = None
 | 
				
			||||||
 | 
					        print(self.goodbye)
 | 
				
			||||||
 | 
					        if winner:        
 | 
				
			||||||
 | 
					            print(f"Congratulations to {winner.name}.")
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            print("Nobody won this game.")
 | 
				
			||||||
		Reference in New Issue
	
	Block a user