Initial commit

This commit is contained in:
cchung 2023-08-25 14:34:55 +00:00
commit da4a3bc4c0
15 changed files with 503 additions and 0 deletions

9
.commit_template Normal file
View 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
View File

@ -0,0 +1,3 @@
*.swp
*.swo
**/__pycache__/*

34
nim/game_stub.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"

View 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}")

View 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
View 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
View 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
View 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.")