Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions Game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import numpy as np
from State import State

class Game:
def __init__(self, distances: np.array):
"""
The game is a TSP. A TSP can be described as a complete graph where each node is numbered with
numbers 0, ..., n_nodes -1 and each edge (i, j) has as attribute the distance between nodes i and j.
That is the Game is fully described by providing the distance matrix between nodes.

:param distances: np.array, distance matrix.
"""
self.distances = distances
self.n_nodes = self.distances.shape[0]

def available_actions(self, state: State) -> list[int]:
"""
Returns the list of available action at a given state.

:param state: State, state of the game.
:return: list, all available actions.
"""
return [a for a in range(self.n_nodes) if a not in state.visited_nodes]

def game_over(self, state: State) -> bool:
"""
True if the state is a final state, i.e. the game is over, False otherwise.

:param state: State, state of the game.
:return: bool, True for game over, False otherwise.
"""
return self.n_nodes == len(state.visited_nodes)

def step(self, state: State, action: int) -> State:
"""
Gives the new state achieved by performing the passed action from state.

:param state: State, state of the game.
:param action: int, action to be performed. Should be a feasible action.
:return: State, the new state reached.
"""
visited_nodes = state.visited_nodes + [action]
return State(action, self.distances[action, :], visited_nodes)

def get_objective(self, state: State) -> float:
"""
Give the lenght of the tour if the state is a final state, else raise an error.
:param state: State, state of the game.
:return: float, total length of the tour.
"""
if not self.game_over(state):
raise Exception("The objective of a partial solution is trying to be computed.")

obj = self.distances[state.current_node, 0]
for node_idx in range(self.n_nodes - 1):
obj += self.distances[state.visited_nodes[node_idx], state.visited_nodes[node_idx + 1]]

return obj

def score(self, state: State, opponent_objective: float) -> int:
"""
Return if the game is won or not by the player. 1 if the game is won, -1 otherwise.

:param state: State, state of the game.
:param opponent_objective: float, lenght of the tour found by the opponent.
:return: int, 1 if the lenght of the tour of the player is less or equal to the opponent's, -1 otherwise.
"""
player_objective = self.get_objective(state)
if player_objective <= opponent_objective:
return 1
return -1

11 changes: 11 additions & 0 deletions State.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import numpy as np


class State:
def __init__(self, current_node: int, node_distances: np.array, visited_nodes: list[int]):
self.current_node = current_node
self.node_distances = node_distances
self.visited_nodes = visited_nodes

def __len__(self):
return self.node_distances.shape[0] + 1
110 changes: 110 additions & 0 deletions unittest/test_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import unittest

import numpy as np

from Game import Game
from State import State

np.random.seed(42)

n_nodes = 4
A = np.random.rand(n_nodes, n_nodes)

class TestAvailableActions(unittest.TestCase):
def setUp(self) -> None:
self.distances = (A.T * A) / 2
self.game = Game(self.distances)

def test_available_actions(self):
state = State(1, self.distances[:, 1], [0, 1])
avail_expected = [2, 3]
avail_returned = self.game.available_actions(state)
self.assertEqual(avail_returned, avail_expected)

def test_no_actions(self):
state = State(1, self.distances[:, 1], [0, 3, 2, 1])
avail_expected = []
avail_returned = self.game.available_actions(state)
self.assertEqual(avail_returned, avail_expected)

class TestGameOver(unittest.TestCase):
def setUp(self) -> None:
self.distances = (A.T * A) / 2
self.game = Game(self.distances)

self.fixtures = (
(State(1, self.distances[:, 1], [0, 1]), False),
(State(1, self.distances[:, 1], [0, 3, 2, 1]), True),
)

def test_fixtures(self):
for state, expected in self.fixtures:
if expected:
self.assertTrue(self.game.game_over(state))
else:
self.assertFalse(self.game.game_over(state))


class TestStep(unittest.TestCase):
def setUp(self) -> None:
self.distances = (A.T * A) / 2
self.game = Game(self.distances)

self.fixtures = (
(State(1, self.distances[:, 1], [0, 1]), 2, State(2, self.distances[:, 2], [0, 1, 2])),
(State(1, self.distances[:, 1], [0, 1]), 3, State(3, self.distances[:, 3], [0, 1, 3])),
)

def test_fixtures(self):
for state, action, expected in self.fixtures:
new_state = self.game.step(state, action)
self.assertEqual(new_state.current_node, expected.current_node)
self.assertTrue(all(new_state.node_distances == expected.node_distances))
self.assertEqual(new_state.visited_nodes, expected.visited_nodes)


class TestGetObjective(unittest.TestCase):
def setUp(self) -> None:
self.distances = np.array(
[
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7]
]
)
self.game = Game(self.distances)
self.state = State(3, self.distances[:, 3], [0, 1, 2, 3])

def test_objective(self):
obj_returend = self.game.get_objective(self.state)
obj_expected = 16
self.assertEqual(obj_expected, obj_returend)


class TestScore(unittest.TestCase):
def setUp(self) -> None:
self.distances = np.array(
[
[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7]
]
)
self.game = Game(self.distances)
self.state = State(3, self.distances[:, 3], [0, 1, 2, 3])
self.fixtures = (
(15, -1),
(16, 1),
(17, 1),
)

def test_fixtures(self):
for opponent_obj, score_expected in self.fixtures:
score_returend = self.game.score(self.state, opponent_obj)
self.assertEqual(score_returend, score_expected)


if __name__ == "__main__":
unittest.main()