diff --git a/README.md b/README.md
index aaf82d5..f488db0 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,24 @@
# Bot-Man-Toe
-Bot-Man-Toe is an attempt to create a way for players to play games against
-themselves, other players, or self-trained AI players.
+Write a script that plays tic-tac-toe, and see how well it performs against
+other programs!
-## Technology stack
+- [🚀 Write your own agent](agents/README.md)
+- [🖊 Compare how well your agent performs](/elo_tracker/README.md)
+- [🌐 Publish your agent to the internet](/pyserver/README.md)
-Counterintuitively, the **servers** are participants to a game. The **clients**
-are programs or browsers that mediate matches between servers.
+## Get started
-## More
+1. Clone this repository.
+2. Run `python client.py` in the terminal and let two random agents play
+ against each other.
+3. Copy the example agent and [create your own](agents/README.md).
+4. Play against the AgentOfChaos while you improve your strategy.
+5. Publish your agent. _(optional)_
+6. Compare it against others with the Elo tracker. _(optional)_
-- The discovery contract is documented in `spec/README.md`.
-- Python client helpers live under `pyclient/`.
-- Python server helpers live under `pyserver/`.
+
+## Links
+
+- [📜 API specification](pyserver/spec.md)
+- [🏆 Online ELO tracker](https://elo.noordstar.me/)
diff --git a/agents/README.md b/agents/README.md
new file mode 100644
index 0000000..09e3fab
--- /dev/null
+++ b/agents/README.md
@@ -0,0 +1,70 @@
+# 🚀 Write your own agent!
+
+An **agent** is a Python class that knows how to play one or more games.
+
+Don't worry about writing a perfect strategy. Start with something that works,
+print the incoming game state, and improve it one step at a time.
+
+## Quick start
+
+1. Copy `example.py`.
+2. Rename the file and the `ExampleAgent` class.
+3. Update the agent's name, author and version.
+4. Implement `play_tic_tac_toe()`.
+5. Import your agent in `client.py` and let it play a game.
+
+That's enough to get started.
+
+## Understanding the game
+
+Whenever your agent has to make a move, it receives the current game state as
+a Python dictionary.
+
+Start by printing it:
+
+```py
+print(payload)
+```
+
+Run a few games and watch how the payload changes after every move. Once you
+understand what you're receiving, you can start writing your own strategy!
+
+Your function should return:
+
+```py
+{"move": 7}
+```
+
+where the number is the square you want to play.
+
+## Testing your agent
+
+Open [client.py](/client.py) and replace one of the players with your own agent.
+For example:
+
+```py
+from agents.my_agent import MyAgent
+from agents.chaos import AgentOfChaos
+
+players = [
+ MyAgent(),
+ AgentOfChaos(),
+]
+```
+
+The repository includes `AgentOfChaos`, a very simple opponent that plays
+random moves. It's useful for testing your own agent while you're developing it.
+
+> ⚠️ Don't try to build the perfect player immediately! Agents are easy to
+> improve while you test against other agents. Plus, imperfect agents are
+> typically the most interesting.
+
+Run the client, inspect the output, tweak your algorithm, and repeat. You don't
+need to understand the rest of the project before you can start experimenting.
+
+## What's next?
+
+Once you're happy with your agent, you can:
+
+- [🖊 Compare your agent against other agents with the ELO tracker](/elo_tracker/README.md)
+- [🌐 Publish your agent so other people can play against it](/pyserver/README.md)
diff --git a/agents/example.py b/agents/example.py
new file mode 100644
index 0000000..5659138
--- /dev/null
+++ b/agents/example.py
@@ -0,0 +1,104 @@
+"""
+ This module contains an example agent that you can use to create your own!
+
+ Please copy this file, rename it, and then build it the way you see fit.
+"""
+
+from __future__ import annotations
+
+import random
+
+from .agent import Agent, Payload
+
+class ExampleAgent(Agent):
+ """
+ Describe here what your agent does and how it behaves! This will help
+ others understand how your agent works.
+ """
+
+ def __init__(self) -> None:
+ """
+ Create a custom instance of your agent. This function allows you
+ to define which games your agent can play.
+
+ You may add parameters to the function if your agent requires more
+ information to be able to operate.
+ """
+
+ super().__init__(
+ # Give your bot a name to display in leaderboards
+ name="MY super smart agent",
+
+ # Your name, to give you credit
+ author="Unknown programmer",
+
+ # Update the version to indicate the agent behaves differently.
+ # This will later allow you to compare different versions of your
+ # agent against one another.
+ version="1.0.0",
+
+ # Add extra custom information about the agent to this dictionary.
+ profile={},
+ )
+
+ # Indicate that you're willing to play tic-tac-toe
+ # Remove this if you don't want your bot to participate there.
+ self.add_tic_tac_toe(on_move=self.play_tic_tac_toe, profile={})
+
+ @staticmethod
+ def play_tic_tac_toe(payload : Payload) -> Payload:
+ """
+ Play a game of tic-tac-toe.
+
+ You receive a payload that looks like this:
+
+ {
+ "1": "X", "2": "", "3": "O",
+ "4": "X", "5": "O", "6": "",
+ "7": "", "8": "", "9": "",
+ "your_token": "X"
+ }
+
+ And you're expected to return a response of which field you'd like to
+ place your piece in. For example, if you wish to place your token in
+ field 7, your response should look like this:
+
+ { "move": 7 }
+
+ The board is arranged as follows:
+
+ 1 | 2 | 3
+ ---+---+---
+ 4 | 5 | 6
+ ---+---+---
+ 7 | 8 | 9
+
+ :param payload: The incoming JSON that contains the game state.
+ :type payload: dict[str, Any]
+ :return: The move you wish to take.
+ :rtype: dict[str, Any]
+ """
+
+ # Try printing the payload to see what it looks like!
+ print(payload)
+
+ options = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, ]
+
+ # 1. Try filtering out the impossible moves!
+ # If an X or O was already placed at a field, remove it from the options
+
+ #
+
+ # 2. Try finding two in a row! If possible, you can try to place the third
+ # item on the board and get 3 in a row.
+
+ #
+
+ # 3. Perhaps you can block the opponent from getting 3 in a row?
+
+ #
+
+ # Now, pick any of the remaining options.
+ # This is just a simple implementation. Naturally, you're welcome to try
+ # your own algorithm.
+ return { "move": random.choice(options) }
diff --git a/client.py b/client.py
index 42cb17a..5fdc847 100644
--- a/client.py
+++ b/client.py
@@ -23,9 +23,14 @@ from __future__ import annotations
import json
import pyclient
-from agents import Agent, RemoteAgent
-from pyclient import PyClient
+# Import your game(s) here
from pyclient.games import TicTacToe
+# ...
+
+# Import your agent(s) here
+from agents import Agent, AgentOfChaos
+# from agents.my_agent import MyAgent
+# ...
def main() -> int:
"""
@@ -34,22 +39,29 @@ def main() -> int:
:return: Exit code
:rtype: int
"""
- c = PyClient(debug=True)
+ # Start the game engine
+ c = pyclient.PyClient(debug=True)
+
+ # Mix and match any agents you'd like.
+ # During development it's usually easiest to play against AgentOfChaos().
players : list[Agent] = [
- RemoteAgent(url="https://bmt001.noordstar.me/"),
- RemoteAgent(url="https://bmt002.noordstar.me/"),
+ AgentOfChaos(),
+ AgentOfChaos(),
]
- out = c.play_game(
+ # Play a given game with your players
+ result = c.play_game(
players=players,
start=TicTacToe.empty(),
)
- inspect_game(out)
+ # Print the game results to the terminal!
+ inspect_game(result)
return 0
+
def inspect_game(game : pyclient.GameReplay) -> None:
"""
Print a diagnostic of a played game to the terminal.
@@ -89,5 +101,6 @@ def inspect_game(game : pyclient.GameReplay) -> None:
print(f"Total turns taken: {len(game.turns)}")
print(f"Result: {final_state.winner()}")
+
if __name__ == "__main__":
raise SystemExit(main())
diff --git a/elo_tracker/README.md b/elo_tracker/README.md
new file mode 100644
index 0000000..1431dfb
--- /dev/null
+++ b/elo_tracker/README.md
@@ -0,0 +1,49 @@
+# ELO tracker
+
+The ELO tracker lets your agent play many games and assigns every participant
+an Elo rating.
+
+## Before you start
+
+The tracker only plays against **remote agents**. Remote agents are available
+through a URL, and usually run on the internet.
+
+If your agent only exists as a local Python class,
+[publish it first](/pyserver/README.md).
+
+## Add players
+
+Open `known_players.json` and add the URLs you want to include.
+
+For example:
+
+```json
+{
+ "players": [
+ "https://my-agent.example",
+ "https://bmt001.noordstar.me"
+ ]
+}
+```
+
+Every listed URL is considered a participant. The ELO tracker will compare
+these players with one another.
+
+## Start the tracker
+
+Run:
+
+```sh
+python elo.py
+```
+
+The tracker will continuously schedule new matches until you stop it.
+
+## Results
+
+Two files are updated while the tracker runs:
+
+* `games.jsonl` stores the played games.
+* `known_players.json` stores the list of participants.
+
+You can stop the tracker with Ctrl+C.
diff --git a/known_players.json b/known_players.json
index 62f409e..de7befa 100644
--- a/known_players.json
+++ b/known_players.json
@@ -1,6 +1,7 @@
{
"players": [
"https://bmt001.noordstar.me",
- "https://bmt002.noordstar.me"
+ "https://bmt002.noordstar.me",
+ "https://bmt003.noordstar.me"
]
}
\ No newline at end of file
diff --git a/pyserver/README.md b/pyserver/README.md
new file mode 100644
index 0000000..9c7512e
--- /dev/null
+++ b/pyserver/README.md
@@ -0,0 +1,36 @@
+# Publishing your agent
+
+Once your agent behaves the way you want, you can expose it over HTTP so other clients can play against it.
+
+You only need this if you want other programs to connect to your agent.
+
+## Use this repository
+
+1. Open `server.py`.
+2. Replace the agent passed to `PyServer` with your own. For example:
+
+```py
+from agents.my_agent import MyAgent
+
+player = PyServer(MyAgent())
+```
+
+3. Start the server. Run:
+
+```sh
+python server.py
+```
+
+By default, the server listens on port `5000`.
+
+4. Open your browser and visit `http://localhost:5000/`. You should receive
+a JSON document describing your agent and the games it supports.
+If that works, your agent is ready to receive game requests.
+
+## Writing your own server
+
+The included server is the easiest way to publish a Python agent.
+
+If you want to implement the protocol yourself
+_(for example in another language)_ you can use the specification in
+[the protocol specification](spec.md).
diff --git a/spec/README.md b/pyserver/spec.md
similarity index 71%
rename from spec/README.md
rename to pyserver/spec.md
index 156d3b9..e918449 100644
--- a/spec/README.md
+++ b/pyserver/spec.md
@@ -3,6 +3,10 @@
This document describes how a Bot-Man-Toe server or client is supposed to
behave.
+**This document requires familiarity with setting up an HTTP server.** Please
+use [the easy implementation](/pyserver/README.md) if you're building
+[a simple agent](/agents/README.md) in this repository.
+
## Terminology
A **server** is a REST API server that hosts a player willing to play games.
@@ -65,3 +69,13 @@ package namespace guidelines.
}
```
+There's a few optional data fields that are specified here:
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| author | string | The name of the person who designed the agent. |
+| version | string | The version of the agent, in case there's an update. |
+| me.noordstar.peanuts.containerized | bool | Whether the agent is running in a container. |
+| me.noordstar.peanuts.is_ai | bool | Whether the agent runs on a trained deep learning model or some artificial intelligence. |
+| me.noordstar.peanuts.agent.version | string | **Deprecated.** Experimental value to demonstrate an agent's version. Please use `version` instead. |
+| me.noordstar.peanuts.author | string | **Deprecated.** Experimental value to demonstrate an agent's author. Please use `author` instead. |
diff --git a/server.py b/server.py
index 9ed7be0..a3c7c5b 100644
--- a/server.py
+++ b/server.py
@@ -15,7 +15,7 @@ def main() -> int:
:return: Exit code
:rtype: int
"""
- player = PyServer(agents.RemoteAgent(url="https://bmt001.noordstar.me/"))
+ player = PyServer(agents.AgentOfChaos())
# Start listening for games
player.start(