import json
import random
from pathlib import Path
from typing import List
import numpy as np
from loguru import logger
from .state import State
from .action import Action
[docs]class Agent:
"""
Class modeling a player
Attributes
----------
grid: numpy.ndarray
Representation of the board as an array of integers.
current_state: ttt.models.State
Current game state
"""
def __init__(self):
self.grid = None
self.current_state = None
[docs] def update_grid(self, grid: np.ndarray) -> None:
"""
Updates `grid` and `current_state` with the given value.
Parameters
----------
grid: numpy.ndarray
Representation of the board as an array of integers.
"""
self.grid: np.ndarray = grid
self.current_state: State = State(grid)
[docs]class CPUAgent(Agent):
"""
Class modeling a CPU player.
Attributes
----------
states: list
List of states. Representation of the game as a Markov Decision Process.
"""
def __init__(self):
super().__init__()
self.states: List[State] = []
[docs] def update_grid(self, grid: np.ndarray) -> None:
"""
Updates `grid`, `current_state` and `mdp` with the given value.
Parameters
----------
grid: numpy.ndarray
Representation of the board as an array of integers.
"""
super().update_grid(grid)
if not self.has_state(self.current_state):
self.add_state(self.current_state)
else:
# Overwrite new State with the saved one
self.current_state = self.get_state(self.grid)
[docs] def get_random_move(self) -> Action:
"""
Generates a random move.
Returns
-------
move: ttt.models.Action
"""
number_of_possible_moves = len(self.current_state.next_states_transitions)
return Action(self.current_state.next_states_transitions[random.randint(0, number_of_possible_moves - 1)])
[docs] def get_best_move(self) -> Action:
"""
Generates a new move using the previous knowledge.
Returns
-------
move: ttt.models.Action
"""
return self.current_state.get_best_move()
[docs] def has_state(self, state: State) -> bool:
"""
Checks if MDP has registered the given state
Parameters
----------
state: ttt.models.State
State to be checked
Returns
-------
is_registered: bool
"""
for entry in self.states:
if np.array_equal(state.grid, entry.grid):
return True
return False
[docs] def add_state(self, state: State) -> None:
"""
Adds the given state to the MDP.
Parameters
----------
state: ttt.models.State
State to be added.
"""
self.states.append(State(state.grid))
[docs] def get_state(self, grid: np.ndarray) -> State:
"""
Gets the state with the given grid.
Parameters
----------
grid: numpy.ndarray
Representation of the board.
Returns
-------
state: ttt.models.State
State containing the given grid.
"""
for state in self.states:
if np.array_equal(grid, state.grid):
return state
raise ValueError(f"Grid {grid} could not be found in saved states "
f"{[state.grid for state in self.states]}")
[docs] def update_state(self, state: State) -> None:
"""
Updates the MDPs state with the given one.
Parameters
----------
state: ttt.models.State
State to update MDPs with.
"""
for index, entry in enumerate(self.states):
if np.array_equal(state.grid, entry.grid):
entry.next_states_values = state.next_states_values.copy()
self.states[index] = entry
[docs] def serialize(self) -> dict:
"""
Serializes this Agent into a dict.
Returns
-------
serializable_dict: dict
"""
return {
"states": [state.serialize() for state in self.states]
}
[docs] def deserialize(self, json_data: dict) -> None:
"""
Dumps specified `json_data` to the agent.
Parameters
----------
json_data: dict
Dictionary with the following format: {mdp: dict}
"""
for serialized_state in json_data["states"]:
state = State()
state.deserialize(serialized_state)
self.states.append(state)
logger.debug(f"Loaded agent")
[docs] def save(self, path: str) -> None:
"""
Saves model's params as a JSON file.
Parameters
----------
path: str
Where to save agent params.
"""
with open(path, 'w') as fp:
json.dump(self.serialize(), fp)
[docs] def load(self, path: str) -> None:
"""
Loads a JSON file containing agent's params.
Parameters
----------
path: str
Path of the JSON file.
"""
data = json.loads(Path(path).read_text())
self.deserialize(data)
[docs]class HumanAgent(Agent):
"""
Class modeling a human player.
Attributes
----------
grid: list
Representation of the board as list of integers.
"""
def __init__(self):
super().__init__()
[docs] def get_next_move(self) -> Action:
"""
Gets the user move.
Returns
-------
move: int
"""
possible_moves = [action.value + 1 for action in self.current_state.next_states_transitions]
logger.info(f"Which cell do you want to mark? {possible_moves}: ")
move = int(input())
while move not in possible_moves:
logger.info(f"The cell is already marked. Pleas, select another cell.\n\n"
f"Which cell do you want to mark? {possible_moves}: ")
move = int(input())
return Action(move - 1)