88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
import typing
|
|
|
|
State = tuple[int, list[str | int]] # Tuple of player (whose turn it is),
|
|
# and the buckets (as str)
|
|
# or the number in a bucket
|
|
Action = str | int # Bucket choice (as str) or choice of number
|
|
|
|
|
|
class Game:
|
|
def initial_state(self) -> State:
|
|
return 0, ["A", "B", "C"]
|
|
|
|
def to_move(self, state: State) -> int:
|
|
player, _ = state
|
|
return player
|
|
|
|
def actions(self, state: State) -> list[Action]:
|
|
_, actions = state
|
|
return actions
|
|
|
|
def result(self, state: State, action: Action) -> State:
|
|
if action == "A":
|
|
return (self.to_move(state) + 1) % 2, [-50, 50]
|
|
elif action == "B":
|
|
return (self.to_move(state) + 1) % 2, [3, 1]
|
|
elif action == "C":
|
|
return (self.to_move(state) + 1) % 2, [-5, 15]
|
|
assert type(action) is int
|
|
return (self.to_move(state) + 1) % 2, [action]
|
|
|
|
def is_terminal(self, state: State) -> bool:
|
|
_, actions = state
|
|
return len(actions) == 1
|
|
|
|
def utility(self, state: State, player: int) -> float:
|
|
assert self.is_terminal(state)
|
|
_, actions = state
|
|
assert type(actions[0]) is int
|
|
return actions[0] if player == self.to_move(state) else -actions[0]
|
|
|
|
def print(self, state):
|
|
print(f"The state is {state} and ", end="")
|
|
if self.is_terminal(state):
|
|
print(f"P1's utility is {self.utility(state, 0)}")
|
|
else:
|
|
print(f"it is P{self.to_move(state)+1}'s turn")
|
|
|
|
|
|
def minimax_search(game: Game, state: State) -> Action | None:
|
|
_, result = max_value(game, state)
|
|
return result
|
|
|
|
|
|
def max_value(game: Game, state: State) -> tuple[float, Action | None]:
|
|
if game.is_terminal(state):
|
|
return game.utility(state, player), None
|
|
v, move = float("-inf"), float("-inf")
|
|
for a in game.actions(state):
|
|
v2, a2 = min_value(game, game.result(state, a))
|
|
if v2 > v:
|
|
v, move = v2, a
|
|
return typing.cast(float, v), typing.cast(Action, move)
|
|
|
|
|
|
def min_value(game: Game, state: State) -> tuple[float, Action | None]:
|
|
if game.is_terminal(state):
|
|
return game.utility(state, player), None
|
|
v, move = float("+inf"), float("+inf")
|
|
for a in game.actions(state):
|
|
v2, a2 = max_value(game, game.result(state, a))
|
|
if v2 < v:
|
|
v, move = v2, a
|
|
return typing.cast(float, v), typing.cast(Action, move)
|
|
|
|
|
|
game = Game()
|
|
|
|
state = game.initial_state()
|
|
game.print(state)
|
|
while not game.is_terminal(state):
|
|
player = game.to_move(state)
|
|
action = minimax_search(game, state) # The player whose turn it is
|
|
# is the MAX player
|
|
print(f"P{player+1}'s action: {action}")
|
|
assert action is not None
|
|
state = game.result(state, action)
|
|
game.print(state)
|