generated from mwc/lab_tic_tac_toe
Initial commit
This commit is contained in:
commit
b703fd53f3
|
@ -0,0 +1,23 @@
|
|||
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Write your entire commit message above this line.
|
||||
#
|
||||
# The first line should be a quick description of what you changed.
|
||||
# Then leave a blank line.
|
||||
# Then, taking as many lines as you want, answer the questions
|
||||
# corresponding to your checkpoint.
|
||||
#
|
||||
# Checkpoint 1:
|
||||
# - This is the first time you have been asked to read a substantial amount
|
||||
# of code written by someone else. What was this experience like for you?
|
||||
# Did you find any strategies which made it easier to make sense of the code?
|
||||
#
|
||||
# Checkpoint 2:
|
||||
# - Describe the strategy you used to check for a winner.
|
||||
#
|
||||
# Checkpoint 3:
|
||||
# - Playing tic-tac-toe is pretty easy, even for children, but it takes a lot
|
||||
# of work to get a computer to play well. How did your awareness of your
|
||||
# own cognition change as you worked on this lab?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
*.swp
|
||||
*.swo
|
||||
**/__pycache__/*
|
|
@ -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
|
|
@ -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
|
|
@ -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}!")
|
|
@ -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?
|
||||
|
||||
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,34 @@
|
|||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
|
||||
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
groups = ["main"]
|
||||
markers = "platform_system == \"Windows\""
|
||||
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.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "0272075b8c7e01c3558d126d3efff1c07b71bcde638baf2353e2f48fa2bf5db5"
|
|
@ -0,0 +1,21 @@
|
|||
[project]
|
||||
name = "lab-tic-tac-toe"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Chris Proctor",email = "chris@chrisproctor.net"}
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<4.0"
|
||||
dependencies = [
|
||||
"click (>=8.1.8,<9.0.0)"
|
||||
]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
package-mode = false
|
|
@ -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}")
|
||||
|
||||
|
||||
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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.")
|
Loading…
Reference in New Issue