Repository: munichpavel/clovek-ne-jezi-se

Issue:
Add choose-furthest-along agent
TODOs

- [ ] Add distance ~~to home~~ WRONG: distance to last node on player subgraph method

If I had distance to home, then I would only advance players in the home area after all players on main board are already advanced.

Comments:

Patch:
diff --git a/clovek_ne_jezi_se/agents.py b/clovek_ne_jezi_se/agents.py
index e079f75..bf602f4 100644
--- a/clovek_ne_jezi_se/agents.py
+++ b/clovek_ne_jezi_se/agents.py
@@ -7,6 +7,7 @@ from random import randint
 import attr
 
 import matplotlib.pyplot as plt
+import numpy as np
 
 from clovek_ne_jezi_se.game_state import GameState, MoveContainer
 from clovek_ne_jezi_se.log_handler import handler
@@ -54,7 +55,7 @@ class Player:
     @abc.abstractmethod
     def choose_move_idx(
         self, game_state: 'GameState',
-        allowed_moves: Sequence['MoveContainer']
+        allowed_moves: Sequence[Sequence['MoveContainer']]
     ) -> int:
         return
 
@@ -86,8 +87,33 @@ class RandomPlayer(Player):
     """Player that selects uniformly randomly from allowed moves"""
     def choose_move_idx(
         self, game_state: 'GameState',
-        allowed_moves: Sequence['MoveContainer']
+        allowed_moves: Sequence[Sequence['MoveContainer']]
     ) -> int:
-        """TODO: Test???"""
+        """TODO: Test me???"""
         idx = randint(0, len(allowed_moves)-1)
         return idx
+
+
+@attr.s
+class FurthestAlongPlayer(Player):
+    def choose_move_idx(
+        self, game_state: 'GameState',
+        allowed_moves: Sequence[Sequence['MoveContainer']]
+    ) -> int:
+        """
+        Return index for move that is closes to the player's last home space
+        """
+        player_from_moves = []
+        for move_components in allowed_moves:
+            for move_component in move_components:
+                if move_component.from_space.occupied_by == self.name:
+                    player_from_moves.append(move_component)
+
+        player_from_spaces = [move.from_space for move in player_from_moves]
+
+        distances_to_end = [
+            game_state.distance_to_end(space) for space in player_from_spaces
+        ]
+        idx_furthest_along = np.argmin(distances_to_end)
+
+        return idx_furthest_along
diff --git a/clovek_ne_jezi_se/game_state.py b/clovek_ne_jezi_se/game_state.py
index bfcf59b..f50c4ff 100644
--- a/clovek_ne_jezi_se/game_state.py
+++ b/clovek_ne_jezi_se/game_state.py
@@ -556,6 +556,23 @@ class GameState:
         )
         return home_count == self.pieces_per_player
 
+    def distance_to_end(self, board_space: 'BoardSpace') -> int:
+
+        # Get player subgraph
+        player_subgraph_query_paramses = \
+            self._get_player_subgraph_query_paramses(board_space.occupied_by)
+
+        player_subgraph_view = get_filtered_subgraph_view(
+            self._graph, player_subgraph_query_paramses
+        )
+        space_node_name = self._get_board_space_node_name(board_space)
+
+        successor_nodes = nx.dfs_successors(
+            player_subgraph_view, source=space_node_name
+        )
+
+        return len(successor_nodes)
+
     # Visualization
     def draw(
         self, figsize=(8, 6), with_labels=False,
diff --git a/docs/source/index.rst b/docs/source/index.rst
index f599165..9edd497 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -11,6 +11,7 @@ Welcome to clovek-ne-jezi-se's documentation!
    :caption: Contents:
 
    INSTALL
+   EXPERIMENTS
    api
 
 
diff --git a/notebooks/evaluate-agents.ipynb b/notebooks/evaluate-agents.ipynb
index 3438699..58f3bea 100644
--- a/notebooks/evaluate-agents.ipynb
+++ b/notebooks/evaluate-agents.ipynb
@@ -19,11 +19,14 @@
     "import os\n",
     "from pathlib import Path\n",
     "\n",
+    "import pandas as pd\n",
+    "import numpy as np\n",
+    "\n",
     "import mlflow\n",
     "\n",
     "import matplotlib.pyplot as plt\n",
     "\n",
-    "from clovek_ne_jezi_se.agents import RandomPlayer"
+    "from clovek_ne_jezi_se.agents import FurthestAlongPlayer, RandomPlayer"
    ]
   },
   {
@@ -62,6 +65,15 @@
     "        \" and params.main_board_section_length = 1\"\n",
     "    )\n",
     "\n",
+    "\n",
+    "def get_int_value_counts_from_floats(series_of_floats: pd.Series) -> pd.Series:\n",
+    "    counts = series_of_floats.value_counts(dropna=True)\n",
+    "    # Convert index to ints\n",
+    "    counts.index = counts.index.astype(int)\n",
+    "    return counts\n",
+    "\n",
+    "# Tests\n",
+    "\n",
     "def test_dict_to_conjunctive_filter_equality_string():\n",
     "    player_names = ['red', 'blue', 'green', 'yellow']\n",
     "    players = [RandomPlayer(name=name, print_game_state=False) for name in player_names]\n",
@@ -83,8 +95,17 @@
     "        assert filter_string == expected\n",
     "    except AssertionError as err:\n",
     "        print(err, filter_string, ' is not ', expected)\n",
+    "   \n",
+    "\n",
+    "def test_get_int_value_counts_from_floats():\n",
+    "    series = pd.Series([np.nan, 3.0, 0.0, 0.0])\n",
+    "    expected = pd.Series(data=[2, 1], index=[0, 3])\n",
+    "    res = get_int_value_counts_from_floats(series)\n",
+    "    pd.testing.assert_series_equal(res, expected)\n",
     "    \n",
-    "test_dict_to_conjunctive_filter_equality_string()"
+    "    \n",
+    "test_dict_to_conjunctive_filter_equality_string()\n",
+    "test_get_int_value_counts_from_floats()"
    ]
   },
   {
@@ -101,32 +122,22 @@
   {
    "cell_type": "code",
    "execution_count": 3,
-   "metadata": {
-    "scrolled": true
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "1    31\n",
-       "0    24\n",
-       "3    24\n",
-       "2    21\n",
-       "Name: metrics.winner_idx, dtype: int64"
-      ]
-     },
-     "execution_count": 3,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
+   "metadata": {},
+   "outputs": [],
    "source": [
-    "player_names = ['red', 'blue', 'green', 'yellow']\n",
-    "players = [RandomPlayer(name=name, print_game_state=False) for name in player_names]\n",
-    "\n",
     "main_board_section_length = 1\n",
     "pieces_per_player = 4\n",
-    "number_of_dice_faces = 6\n",
+    "number_of_dice_faces = 6"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "player_names = ['red', 'blue', 'green', 'yellow']\n",
+    "players = [RandomPlayer(name=name, print_game_state=False) for name in player_names]\n",
     "agents = [player.__class__.__name__ for player in players]\n",
     "\n",
     "run_params = dict(\n",
@@ -141,21 +152,14 @@
     "os.chdir(os.environ['RUNS_PARENT_DIR'])\n",
     "runs = mlflow.search_runs(filter_string=filter_string)\n",
     "\n",
-    "# Remove runs with no winner\n",
-    "mask_winner_isna = runs['metrics.winner_idx'].isna()\n",
-    "runs = runs[~mask_winner_isna]\n",
-    "winner_idxes = runs['metrics.winner_idx'].astype(int)\n",
-    "\n",
-    "# Calculate winner counts\n",
-    "winner_counts = winner_idxes.value_counts()\n",
+    "winner_counts = get_int_value_counts_from_floats(runs['metrics.winner_idx'])\n",
     "names = [agents[idx] + '_' + str(idx) for idx in winner_counts.index]\n",
-    "counts = winner_counts.values.astype(int)\n",
-    "winner_counts"
+    "counts = winner_counts.values"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
@@ -177,6 +181,56 @@
     "plt.title(f'Win count per agent after {winner_counts.sum()} games')\n",
     "plt.xticks(names, names);"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeAAAADTCAYAAABZTcAiAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAAsTAAALEwEAmpwYAAAaIUlEQVR4nO3deZhcVZ3/8feHJEiAyJaeGEKgR0EdhCFoRB3FiT8RAR0Bh8WMCFExMA8M6IAjw4wDLqMZfoO4MC5shgFc2AXJCBlIBCQsCcQsRAlCEELIIvsikOQ7f5zTUimquqq7q/t0Vz6v56mnb527fe89t+p7z7m3bisiMDMzs4G1SekAzMzMNkZOwGZmZgU4AZuZmRXgBGxmZlaAE7CZmVkBTsBmZmYFOAFby0naW9JvS8dh/U/SSEnXSnpK0mWl4zEbSpyArSFJ/yzpf6rKltYp+1hE3BIRbxrYKHtP0hRJt5aOowRJsyUd3YdFHAKMAbaLiEMlTZf01RaFB4Ck90malZP8shrjO/P45yX9RtI+FeN2k3S9pDWS/NADG1ScgK0ZNwN/JWkYgKSxwAhgz6qynfO0VoeSdvrc7QTcFxFrW7EwScNrFD8HXAB8vs5sPwbuAbYD/gW4XFJHHvcycCnw6VbEZ9ZSEeGXX92+gE2B54G35feHAT8EfllVdn8engQ8UjH/MuBkYAHwFPBTYLNu1vcZYAnwDHAv8NZc/hfAbOBJYDHwkYp5ZgNHV7yfAtxa8T6AY4Glef7/ApSX+UdgHfAs8GSdmGYDXwfuBJ4GfgZsWzH+ncBtedm/BiZVzfvvwK+AF4Cdayz/FOB3Fdt8cMW4YcCZwBrgQeD4vD3D8/itgPOBFcBy4KvAsMr9APwn8ESef/887t/zdv8xb/vZdbb9MuCxXHc3A2/J5V8CXiIluWeBY/LwS/n9tXm67YErgNV5/SdULPt04HLg4rxfj64VQ552H2BZVdkbgReBURVltwDHVk23MxBNHOv7Ar/N2/pd0jF+dB73BuAm4A+5Li4Btq46zj9POs6fy3UyBvifXK//C2zT5DEzBXggz/cg8PHS3wN+tf5VPAC/hsYLmAV8Lg+fDXwqf4FXll2Qhyfx6gR8Z/4i3paUXI+ts55DcxJ5OylB7kxqZY0A7gdOJZ0Q/L/85fSmPN9sGifgnwNbAzvmZLBfrWnrxDU7x7UbsEVOKBfncePyl/IBpF6lD+T3HRXz/h54CzAcGFFnu7fP8x+ev8DH5nHHkpLyDsA2+Yu8MgFfBfwgx/VneV8fU7FtL5NOaoYBfw88CqjWfquz7Z8CRgGvAb4JzK8Yd3rXfsjvpwNfrXi/CTAP+Ldcb68nJZYPVsz/MnBQnnZkN3HUSsAHA0uqys4GvlNV1jABA6NJJwEfzfV0Yo7t6IplfCDvhw7Sycg3q47z20lJdxywCrgb2BPYjJS8T2t0zOR6fJpXju2x5JMev9rr1U5dYda/fgm8Nw/vTWpl3FJV9stu5v92RDwaEY8D1wIT6kx3NHBGRNwVyf0R8RCptbAlMC0iXoqIm0gJdXIPtmFaRDwZEb8nnVDUi6GeiyJiUUQ8B3wROCx3wR8BzIiIGRGxPiJmAnNJX65dpkfE4ohYGxEvVy84Ii7L+2d9RPyU1FLfK48+DPhWRDwSEU8A07rmkzQmr+ezEfFcRKwCzgI+VrH4hyLi3IhYB1xI+kIf0+xGR8QFEfFMRLxISph7SNqqydnfTjoR+XKutweAc6vimxMRV+dtf6HZuLItSa3VSk+RThh66gBgcURcGalL/duklj8A+VicGREvRsRq4BvAX1ct4zsRsTIilpM+H3dExD0R8UfSidKeebpGx8x6YDdJIyNiRUQs7sX22CDnBGzNuhl4j6RtSV+oS0ndZ3+Vy3aj++u/j1UMP0/64qxlPKkrttr2wMMRsb6i7CFSS6JZzcZQz8NV6x5BajXtBBwq6cmuF/AeUqKrNe+rSDpS0vyK+XfLy4a87XWW1dU7sKJi3h+QWsJdKpPI83mwqW2XNEzSNEm/k/Q0qZVHRWyN7ARsX7VvTmXDE4Bu900DzwKvrSp7Lal3pKc22M8REcAjXe8ljZH0E0nL8764mFfvh5UVwy/UeN+13+seM/kE73BSz8cKSddJenMvtscGuVo3PJjVMod0rfEzpGuZRMTTkh7NZY9GxIMtWM/DpGtt1R4FxkvapCIJ7wjcl4efAzavmP51PVhns3fHjq8Y3pHUPbmGFPNFEfGZ3qxD0k6kVuH7Sa3BdZLmk7rgIV3b3aFOHA+TroGOjt7dCNVo2/8OOJDc/Us6Bp6oiK3R8h4GHoyIXfoQQ3cWA6+XNCoiupLuHsCPerGsDfazJLHhfv8aKdbdI+JxSQeRurt7o9tjJiKuB66XNJJ0Tf9cUi+TtRG3gK0puWtwLvCPpK61Lrfmslbd/XwecLKkt+U7hnfOCeoOUqv1nySNkDQJ+BvgJ3m++cBHJW0uaWd6dtfrSmAHSZs2mO4ISbtK2hz4MnB57ta9GPgbSR/MLcbNJE2StEP3i/uTLUhf7KsBJH2S1ALucilwoqRxkrYGvtA1IiJWADcAZ0p6raRNJL1BUnXXaD0rSddl6xlFSvB/IJ3gfK2Hy7sTeEbSF/Jvhoflnwa9vcn4yNu0Gamlr7x/NwWIiPtIdX9aLj8Y+EvSNfquu843I11/Jk/zmjqrug7YXdJB+W7s49jwRG4UqcX9lKRx1L8ruxl1j5nc0j5Q0hakff8sqUva2owTsPXEL0ldm5W/mb0ll7UkAUfEZaSbu35E6ka8mnS38UukhLs/qdX5XeDIiPhNnvUs0t23K0nXOS/pwWpvIrWkHpO0ppvpLiLdZPQY6aaaE3LMD5NaiaeSkujDpC/npj5fEXEv6S7nOTn+3cm9DNm5pCS7gPRzmxnAWtIdzABHkhLMvaTW6eVs2P3dnW8Bh0h6QtK3a4z/b1J3+/K8/NsbLO98YNfcrXp1PkH5MOl6+4OkujuP1JJu1ntJ3bczSD0PL5D2R5ePARNJ2z4NOCRfo4XU1fsCqX7JwzUfEhMRa0g3w51BOuHYlXTS+WKe5EvAW0nXmK8DruzBNlSvq7tjZhPSSe2jwOOk68x/39t12eDVdSekmXVD0mzS3b7nDYJY9ge+HxE7lY6lneXfaz9C+gnQrNLxWPtxC9hskMtdtwdIGp67Pk8j3VFrLZa7hLfO3dSnkq51N2r1m/WKE7DZ4CdS9+cTpC7oJaTf1VrrvYt0F/4a0iWPg3rx0yizprgL2szMrAC3gM3MzApwAjYzMytgQB/EMXr06Ojs7BzIVZqZmRUzb968NRHRUWvcgCbgzs5O5s6dO5CrNDMzK0bSQ/XGuQvazMysACdgMzOzApyAzczMCnACNjMzK8AJ2MzMrAAnYDMzswIG9GdIrdZ5ynWlQ2hLy6Z9qHQIZmZtzy1gMzOzApyAzczMCnACNjMzK8AJ2MzMrAAnYDMzswKcgM3MzApwAjYzMyvACdjMzKwAJ2AzM7MCnIDNzMwKGNKPorShw48NbT0/MtRsaHML2MzMrICGCVjSeEmzJN0rabGkE3P56ZKWS5qfXwf0f7hmZmbtoZku6LXASRFxt6RRwDxJM/O4syLiP/svPDMzs/bUMAFHxApgRR5+RtISYFx/B2ZmZtbOenQTlqROYE/gDuDdwPGSjgTmklrJT9SYZyowFWDHHXfsa7xm1s98w1zr+YY5q6Xpm7AkbQlcAXw2Ip4Gvge8AZhAaiGfWWu+iDgnIiZGxMSOjo6+R2xmZtYGmkrAkkaQku8lEXElQESsjIh1EbEeOBfYq//CNDMzay/N3AUt4HxgSUR8o6J8bMVkBwOLWh+emZlZe2rmGvC7gU8ACyXNz2WnApMlTQACWAYc0w/xmZmZtaVm7oK+FVCNUTNaH46ZmdnGwU/CMjMzK8AJ2MzMrAAnYDMzswKcgM3MzApwAjYzMyvACdjMzKwAJ2AzM7MCnIDNzMwKcAI2MzMrwAnYzMysACdgMzOzApr5ZwxmZjYIdZ5yXekQ2s6yaR8asHW5BWxmZlaAE7CZmVkBTsBmZmYFOAGbmZkV4ARsZmZWgBOwmZlZAQ0TsKTxkmZJulfSYkkn5vJtJc2UtDT/3ab/wzUzM2sPzbSA1wInRcSuwDuB4yTtCpwC3BgRuwA35vdmZmbWhIYJOCJWRMTdefgZYAkwDjgQuDBPdiFwUD/FaGZm1nZ6dA1YUiewJ3AHMCYiVuRRjwFjWhuamZlZ+2o6AUvaErgC+GxEPF05LiICiDrzTZU0V9Lc1atX9ylYMzOzdtFUApY0gpR8L4mIK3PxSklj8/ixwKpa80bEORExMSImdnR0tCJmMzOzIa+Zu6AFnA8siYhvVIy6BjgqDx8F/Kz14ZmZmbWnZv4b0ruBTwALJc3PZacC04BLJX0aeAg4rF8iNDMza0MNE3BE3Aqozuj3tzYcMzOzjYOfhGVmZlaAE7CZmVkBTsBmZmYFOAGbmZkV4ARsZmZWgBOwmZlZAU7AZmZmBTgBm5mZFeAEbGZmVoATsJmZWQFOwGZmZgU4AZuZmRXgBGxmZlaAE7CZmVkBTsBmZmYFOAGbmZkV4ARsZmZWgBOwmZlZAU7AZmZmBTRMwJIukLRK0qKKstMlLZc0P78O6N8wzczM2kszLeDpwH41ys+KiAn5NaO1YZmZmbW3hgk4Im4GHh+AWMzMzDYafbkGfLykBbmLepuWRWRmZrYR6G0C/h7wBmACsAI4s96EkqZKmitp7urVq3u5OjMzs/bSqwQcESsjYl1ErAfOBfbqZtpzImJiREzs6OjobZxmZmZtpVcJWNLYircHA4vqTWtmZmavNrzRBJJ+DEwCRkt6BDgNmCRpAhDAMuCY/gvRzMys/TRMwBExuUbx+f0Qi5mZ2UbDT8IyMzMrwAnYzMysACdgMzOzApyAzczMCnACNjMzK8AJ2MzMrAAnYDMzswKcgM3MzApwAjYzMyvACdjMzKwAJ2AzM7MCnIDNzMwKcAI2MzMrwAnYzMysACdgMzOzApyAzczMCnACNjMzK8AJ2MzMrAAnYDMzswIaJmBJF0haJWlRRdm2kmZKWpr/btO/YZqZmbWXZlrA04H9qspOAW6MiF2AG/N7MzMza1LDBBwRNwOPVxUfCFyYhy8EDmptWGZmZu2tt9eAx0TEijz8GDCm3oSSpkqaK2nu6tWre7k6MzOz9tLnm7AiIoDoZvw5ETExIiZ2dHT0dXVmZmZtobcJeKWksQD576rWhWRmZtb+epuArwGOysNHAT9rTThmZmYbh2Z+hvRjYA7wJkmPSPo0MA34gKSlwD75vZmZmTVpeKMJImJynVHvb3EsZmZmGw0/CcvMzKwAJ2AzM7MCnIDNzMwKcAI2MzMrwAnYzMysACdgMzOzApyAzczMCnACNjMzK8AJ2MzMrAAnYDMzswKcgM3MzApwAjYzMyvACdjMzKwAJ2AzM7MCnIDNzMwKcAI2MzMrwAnYzMysACdgMzOzApyAzczMChjel5klLQOeAdYBayNiYiuCMjMza3d9SsDZ+yJiTQuWY2ZmttFwF7SZmVkBfU3AAdwgaZ6kqbUmkDRV0lxJc1evXt3H1ZmZmbWHvibg90TEW4H9geMkvbd6gog4JyImRsTEjo6OPq7OzMysPfQpAUfE8vx3FXAVsFcrgjIzM2t3vU7AkraQNKprGNgXWNSqwMzMzNpZX+6CHgNcJalrOT+KiF+0JCozM7M21+sEHBEPAHu0MBYzM7ONhn+GZGZmVoATsJmZWQFOwGZmZgU4AZuZmRXgBGxmZlaAE7CZmVkBTsBmZmYFOAGbmZkV4ARsZmZWgBOwmZlZAU7AZmZmBTgBm5mZFeAEbGZmVoATsJmZWQFOwGZmZgU4AZuZmRXgBGxmZlaAE7CZmVkBfUrAkvaT9FtJ90s6pVVBmZmZtbteJ2BJw4D/AvYHdgUmS9q1VYGZmZm1s760gPcC7o+IByLiJeAnwIGtCcvMzKy99SUBjwMernj/SC4zMzOzBob39wokTQWm5rfPSvptf69zkBoNrCkdRDP0H6UjKG5I1JXrCXBdDRVDop6gX+pqp3oj+pKAlwPjK97vkMs2EBHnAOf0YT1tQdLciJhYOg5rzHU1dLiuhgbXU2196YK+C9hF0p9L2hT4GHBNa8IyMzNrb71uAUfEWknHA9cDw4ALImJxyyIzMzNrY326BhwRM4AZLYql3W303fBDiOtq6HBdDQ2upxoUEaVjMDMz2+j4UZRmZmYFOAGbmZkVMKgSsKR1kuZXvDp7MO8ESQdUvD9d0sktiGmKpO2rykZLelnSsVXlyySN7us6q5Y5SdJTeX8skXRaRfnPW7muHsR0lKSl+XVUD+ftquNFkq6VtHWLYpoi6ewWLWuZpIWSFki6QdLrKspbWr9NxnN8ft56DOT6XVe9iud8Sb/O8VwuacsBWq/rqufxXJL/l8EiSRdIGjHQMQyqBAy8EBETKl7LmplJ0nBgAnBAg0l7YwqwfVXZocDtwOR+WF8tt0TEBGAicISkt/b3CvM+rVW+LXAa8A7S40hPk7RNDxbdVce7AY8Dx/U52P7xvoj4S2AucOpArDA/X72WXwH7AA8NRBwVXFd1dFNXn4uIPXI8vweOH4h4cF3V1U1dXQK8GdgdGAkcPRDxVBpsCfhVKs+OJE2UNDsPny7pIkm/Ai4Cvgwcns8CD8+z7ypptqQHJJ1QscwjJN2Zp/2BpGH5NT2fDS2U9DlJh5CS3iV52pF5EZOBk4BxknaoE/c/5mUtkvTZXNaZW7HnSlqczwJH5nFvz2eG8yX9f0mLqpcZEc8B84Cdq9a1l6Q5ku6RdJukN+XymyVNqJjuVkl7SNoin/Hdmec5MI+fIukaSTcBN9apkg8CMyPi8Yh4ApgJ7Fdn2kbmkB9f2s02TJF0paRfKLW4z6jYnk9Kuk/SncC7K8o7Jd2U9+eNknbM5dMlfU/S7fmYmJT3wxJJ0+vEeDNV+zsv62pJ83I9Ts1ln5L0zYppPiPprDz8qmMulz8r6UxJvwbeVSuAiLin2ZPRfuS6aq6uns7TivSlXuIuV9dVc3U1IzLgTtLDpAZWRAyaF7AOmJ9fV+WyZcDoPDwRmJ2HTyclo5H5/RTg7IplnQ7cBryG9Bi0PwAjgL8ArgVG5Om+CxwJvI2UWLrm3zr/nQ1MrCgfDyzNw18DTqoYtyyv623AQmALYEtgMbAn0AmsBSbk6S8FjsjDi4B35eFpwKI8PAn4eR7eLq/jLVXlrwWG5+F9gCvy8FHAN/PwG4G5FXF3rXdr4L4c6xTSM7237aaOTgb+teL9F4GTe1DHz+a/w4DLgP0abMMU4AFgK2AzUitwPDCW1MLoADYltRLPzvNcCxyVhz8FXJ2Hp5P+aYhI/zjkadLZ7yakY6mrXpbxyjF3NvAfNcq3zX9H5rrbLtf173jl2LotL7/mMZeHAzisyX33p/UP0OfRddWLugJ+CKwEZgGbu64Gb13laUcAdwN7D9Rnq+vV78+C7qEXInW1NuuaiHihm/HXRcSLwIuSVgFjgPeTEuRd6SSVkcAqUkW+XtJ3gOuAG+os83BS4oR00F0AnFk1zXtIJxDPAUi6Etib9KSwByNifp5uHtCpdL1mVETMyeU/Aj5csby9Jd0DrAemRcRiSZMqxm8FXChpF9KB13Ut4zLgi5I+T/rATM/l+wIf0SvXyDcDdszDMyPi8Trb3gojJc0nnaEvIbWgu9sGgBsj4ikASfeSnq06mnQytjqX/5R0kgHpjPejefgi4IyKZV0bESFpIbAyIhbm+ReTTpDm5+lmSVoHLAD+tcZ2nCDp4Dw8HtglIm5X6j34sKQlpC+GhUoPrKl1zEE66byi+11WjOuqF3UVEZ/MLbHvkL4vfthonhZwXfX+c/Vd4OaIuKXJ6VtmsCXgWtbySlf5ZlXjnmsw74sVw+tI2yvgwoj45+qJJe1B6mI9FjiMlLSqTQZeJ+nj+f32knaJiKUNYqkX08h6E1a4JSI+3M34rwCzIuJgpRvXZgNExPOSZpLOSg8jHayQ9sHfRsQG/xhD0jtovE+Xk1rfXXboWl+TXoiICZI2Jz1F7Tjg2/W2IatVj73Vtaz1VctdX7Xc90VEzYfH55OffUg9Fs8rXRbpOjbPI13b+g2vfPHWPeaAP0bEup5vxoBwXW2o6bqKiHWSfgL8EwOTgF1XG2qqrpRuau0Ajmk0bX8Y9NeASd0TXYnjb7uZ7hlgVBPLuxE4RNKfQbqpSNJOSteZN4mIK0hnZl03Ov1puZLeCGwZEeMiojMiOoGv8+qbsW4BDpK0uaQtgINzWU0R8STwTE6AkJ6r3RNb8co/wphSNe480gfxrkjXbCF9QP9B+bRR0p49WNf1wL6StlG6+WrfXNYjEfE8cAJwktINX91tQy13AH8taTuluxcPrRh3G6/sw4/Tzb7vpa2AJ/KXxJuBd3aNiIg7SGfufwf8OBfXPOZaHFO/cV01V1dKdu4aBj5CShgDxnXV/OdK0tGkBtfkiFjfom3okaGQgL8EfEvSXNJZWj2zSDddVd6E9SoRcS8pwd4gaQGpq2Ysqetmdu7GuRjoOquaDnw/l08Grqpa5BVUJeCIuDvPdyfpgD4vIu5psJ2fBs7N69kCeKrB9JXOAL6eu6k3OIuNiHmkazKVZ+FfIXVFLchdRF9pdkW5e/orpH/GcRfw5d52Wed9soC0/+puQ515V5Cu888hXadaUjH6H4BP5vr9BHBib+Lrxi+A4bk7bBrpjvhKlwK/6jrh6eaYa4qkEyQ9QuptWCDpvBZsQ4+4rpoiUnfvQtI9IGNJN4cOKNdV075Puiw5J+eNf+vrBvSUH0U5SEjaMiKezcOnAGMjos8HuNJvmGcDby51lrexUfp99lkRUe9OchskXFdDRzvW1VBoAW8sPpTPwhaRbtj6al8XKOlIUgv8X5x8+5+krSXdR7oe1zZfEu3IdTV0tHNduQVsNUnanXSnY6UXI+Idtaa3vpN0FfDnVcVfiIgeX2O3/uW6GjoGc105AZuZmRXgLmgzM7MCnIDNzMwKcAI2MzMrwAnYzMysACdgMzOzAv4PR3Fxr29LqMEAAAAASUVORK5CYII=\n",
+      "text/plain": [
+       "<Figure size 576x216 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "player_names = ['blue', 'green', 'yellow']\n",
+    "players = (\n",
+    "    [FurthestAlongPlayer(name='red')] + \n",
+    "    [RandomPlayer(name=name, print_game_state=False) for name in player_names]\n",
+    ")\n",
+    "\n",
+    "agents = [player.__class__.__name__ for player in players]\n",
+    "\n",
+    "run_params = dict(\n",
+    "    agents=','.join(agents),\n",
+    "    main_board_section_length=main_board_section_length,\n",
+    "    pieces_per_player=pieces_per_player,\n",
+    "    number_of_dice_faces=number_of_dice_faces\n",
+    ")\n",
+    "filter_string = dict_to_conjunctive_filter_equality_string(run_params, 'params')\n",
+    "\n",
+    "# mlflow.search_runs seemingly must be run from parent directory of mlruns\n",
+    "os.chdir(os.environ['RUNS_PARENT_DIR'])\n",
+    "runs = mlflow.search_runs(filter_string=filter_string)\n",
+    "\n",
+    "winner_counts = get_int_value_counts_from_floats(runs['metrics.winner_idx'])\n",
+    "names = [agents[idx] + '_' + str(idx) for idx in winner_counts.index]\n",
+    "counts = winner_counts.values\n",
+    "\n",
+    "# Plot\n",
+    "plt.figure(figsize=(8,3))\n",
+    "plt.bar(names, counts)\n",
+    "plt.title(f'Win count per agent after {winner_counts.sum()} games')\n",
+    "plt.xticks(names, names);"
+   ]
   }
  ],
  "metadata": {
diff --git a/scripts/play.py b/scripts/play.py
index 397a750..2998a94 100644
--- a/scripts/play.py
+++ b/scripts/play.py
@@ -1,19 +1,19 @@
 import mlflow
-from mlflow.tracking import MlflowClient
-
 
 from clovek_ne_jezi_se.client import Client
-from clovek_ne_jezi_se.agents import HumanPlayer, RandomPlayer
+from clovek_ne_jezi_se.agents import RandomPlayer, FurthestAlongPlayer
+
 
-# TODO put in config file (json)
-player_names = ['red', 'blue', 'green', 'yellow']
+furthest_along_player = FurthestAlongPlayer(name='red')
 
+random_player_names = ['blue', 'green', 'yellow']
 random_players = [
-    RandomPlayer(name=name, print_game_state=False) for name in player_names
+    RandomPlayer(name=name, print_game_state=False)
+    for name in random_player_names
 ]
-players = random_players
+players = [furthest_along_player] + random_players
 
-n_runs = 96
+n_runs = 100
 main_board_section_length = 1
 pieces_per_player = 4
 number_of_dice_faces = 6


Test patch:
diff --git a/tests/test_agents.py b/tests/test_agents.py
new file mode 100644
index 0000000..e840379
--- /dev/null
+++ b/tests/test_agents.py
@@ -0,0 +1,182 @@
+"""Tests for agents, if not already tested in test_client."""
+from copy import deepcopy
+
+import pytest
+
+from clovek_ne_jezi_se.client import Client
+from clovek_ne_jezi_se.game_state import (
+    GameState, MoveContainer, BoardSpace, EMPTY_SYMBOL
+)
+from clovek_ne_jezi_se.agents import FurthestAlongPlayer
+
+
+def assert_game_states_equal(
+    game_state: 'GameState', other: 'GameState'
+) -> bool:
+    waiting = game_state.waiting_areas_to_dict()
+    other_waiting = other.waiting_areas_to_dict()
+
+    assert waiting == other_waiting
+
+    main = game_state.main_spaces_to_list()
+    other_main = other.main_spaces_to_list()
+
+    assert main == other_main
+
+    home = game_state.home_areas_to_dict()
+    other_home = other.home_areas_to_dict()
+
+    assert home == other_home
+
+
+def test_assert_game_states_equal():
+    # Set attributes for repeated use below
+    player_names = ['red', 'blue', 'green', 'yellow']
+    pieces_per_player = 4
+    section_length = 4
+    game_state = GameState(
+        player_names=player_names, pieces_per_player=pieces_per_player,
+        section_length=section_length
+    )
+    game_state.initialize()
+
+    assert_game_states_equal(game_state, game_state)
+
+    other = deepcopy(game_state)
+    other.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=0,
+                occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=0,
+                occupied_by=EMPTY_SYMBOL,
+                allowed_occupants=player_names + [EMPTY_SYMBOL]
+            )
+        ))
+
+    with pytest.raises(AssertionError):
+        assert_game_states_equal(game_state, other)
+
+
+class TestAgents:
+    player_names = ['red']
+    players = [
+        FurthestAlongPlayer(name=name, print_game_state=False)
+        for name in player_names
+    ]
+
+    client = Client(players=players)
+    client.initialize()
+
+    def test_furthest_along_choose_move(self, monkeypatch):
+        played_client = deepcopy(self.client)
+        expected_client = deepcopy(self.client)
+
+        # Move red players to main board
+        played_game_state = played_client.get_game_state()
+        idx_main_ahead = 3
+        idx_main_behind = 1
+        idx_home = played_game_state.pieces_per_player-2
+        played_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=0,
+                occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=idx_main_ahead,
+                occupied_by=EMPTY_SYMBOL,
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+            )
+        ))
+        played_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=1,
+                occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=idx_main_behind,
+                occupied_by=EMPTY_SYMBOL,
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+            )
+        ))
+        played_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=2,
+                occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='home', idx=idx_home,
+                occupied_by=EMPTY_SYMBOL,
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            )
+        ))
+
+        # Set roll value to 1
+        roll = 1
+        monkeypatch.setattr(played_client, 'roll', lambda: roll)
+
+        # Play once (red) with fixed (monkeypatched) dice
+        played_client.take_turn()
+
+        expected_game_state = expected_client.get_game_state()
+
+        expected_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=0, occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=idx_main_ahead,
+                occupied_by='red',
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+            )
+        ))
+        expected_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=1, occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=idx_main_behind,
+                occupied_by='red',
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+
+            )
+        ))
+        expected_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='waiting', idx=2,
+                occupied_by='red',
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='home', idx=idx_home + roll,
+                occupied_by=EMPTY_SYMBOL,
+                allowed_occupants=['red', EMPTY_SYMBOL]
+            )
+        ))
+
+        assert_game_states_equal(played_game_state, expected_game_state)
+
+        # Play again red with fixed (monkeypatched) dice
+        played_client.take_turn()
+
+        expected_game_state.do(MoveContainer(
+            from_space=BoardSpace(
+                kind='main', idx=idx_main_ahead,
+                occupied_by='red',
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+            ),
+            to_space=BoardSpace(
+                kind='main', idx=idx_main_ahead + roll,
+                occupied_by='red',
+                allowed_occupants=self.player_names + [EMPTY_SYMBOL]
+            )
+        ))
+
+        assert_game_states_equal(played_game_state, expected_game_state)
diff --git a/tests/test_state.py b/tests/test_state.py
index 063339f..6e06b56 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -7,12 +7,6 @@ from clovek_ne_jezi_se.game_state import (
     EMPTY_SYMBOL, GameState, BoardSpace, MoveContainer
 )
 
-from clovek_ne_jezi_se.utils import (
-    GraphQueryParams,
-    get_filtered_subgraph_view,
-    get_filtered_node_names
-)
-
 
 def test_board_space_errors():
     with pytest.raises(ValueError):
@@ -379,7 +373,6 @@ class TestGameState:
             kind=to_space.kind, idx=to_space.idx, player_name='red'
         ) == post_do_to_space
 
-
     @pytest.mark.parametrize(
         "roll,from_space",
         [
@@ -608,3 +601,34 @@ class TestGameState:
             ))
 
         assert modified_game_state.is_winner(player_name) == expected
+
+    @pytest.mark.parametrize(
+        'board_space,expected',
+        [
+            (
+                BoardSpace(
+                    kind='main', idx=player_prehome_indices['red'],
+                    occupied_by='red',
+                    allowed_occupants=player_names + [EMPTY_SYMBOL]
+                ), 4
+            ),
+            (
+                BoardSpace(
+                    kind='home', idx=pieces_per_player-1,
+                    occupied_by='red',
+                    allowed_occupants=['red', EMPTY_SYMBOL]
+                ), 0
+            ),
+            (
+                BoardSpace(
+                    kind='waiting', idx=0,
+                    occupied_by='red',
+                    allowed_occupants=['red', EMPTY_SYMBOL]
+                ), pieces_per_player * (section_length + 1)
+            )
+        ]
+
+    )
+    def test_distance_to_end(self, board_space, expected):
+        assert self.game_state.distance_to_end(board_space) \
+            == expected