Add improved documentation
parent
87525238ba
commit
73be372a30
27
README.md
27
README.md
|
|
@ -1,15 +1,24 @@
|
||||||
# Bot-Man-Toe
|
# Bot-Man-Toe
|
||||||
|
|
||||||
Bot-Man-Toe is an attempt to create a way for players to play games against
|
Write a script that plays tic-tac-toe, and see how well it performs against
|
||||||
themselves, other players, or self-trained AI players.
|
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**
|
## Get started
|
||||||
are programs or browsers that mediate matches between servers.
|
|
||||||
|
|
||||||
## 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/`.
|
## Links
|
||||||
- Python server helpers live under `pyserver/`.
|
|
||||||
|
- [📜 API specification](pyserver/spec.md)
|
||||||
|
- [🏆 Online ELO tracker](https://elo.noordstar.me/)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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) }
|
||||||
27
client.py
27
client.py
|
|
@ -23,9 +23,14 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import pyclient
|
import pyclient
|
||||||
|
|
||||||
from agents import Agent, RemoteAgent
|
# Import your game(s) here
|
||||||
from pyclient import PyClient
|
|
||||||
from pyclient.games import TicTacToe
|
from pyclient.games import TicTacToe
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Import your agent(s) here
|
||||||
|
from agents import Agent, AgentOfChaos
|
||||||
|
# from agents.my_agent import MyAgent
|
||||||
|
# ...
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""
|
"""
|
||||||
|
|
@ -34,22 +39,29 @@ def main() -> int:
|
||||||
:return: Exit code
|
:return: Exit code
|
||||||
:rtype: int
|
: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] = [
|
players : list[Agent] = [
|
||||||
RemoteAgent(url="https://bmt001.noordstar.me/"),
|
AgentOfChaos(),
|
||||||
RemoteAgent(url="https://bmt002.noordstar.me/"),
|
AgentOfChaos(),
|
||||||
]
|
]
|
||||||
|
|
||||||
out = c.play_game(
|
# Play a given game with your players
|
||||||
|
result = c.play_game(
|
||||||
players=players,
|
players=players,
|
||||||
start=TicTacToe.empty(),
|
start=TicTacToe.empty(),
|
||||||
)
|
)
|
||||||
|
|
||||||
inspect_game(out)
|
# Print the game results to the terminal!
|
||||||
|
inspect_game(result)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def inspect_game(game : pyclient.GameReplay) -> None:
|
def inspect_game(game : pyclient.GameReplay) -> None:
|
||||||
"""
|
"""
|
||||||
Print a diagnostic of a played game to the terminal.
|
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"Total turns taken: {len(game.turns)}")
|
||||||
print(f"Result: {final_state.winner()}")
|
print(f"Result: {final_state.winner()}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
|
|
@ -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 <kbd>Ctrl</kbd>+<kbd>C</kbd>.
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"players": [
|
"players": [
|
||||||
"https://bmt001.noordstar.me",
|
"https://bmt001.noordstar.me",
|
||||||
"https://bmt002.noordstar.me"
|
"https://bmt002.noordstar.me",
|
||||||
|
"https://bmt003.noordstar.me"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
This document describes how a Bot-Man-Toe server or client is supposed to
|
This document describes how a Bot-Man-Toe server or client is supposed to
|
||||||
behave.
|
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
|
## Terminology
|
||||||
|
|
||||||
A **server** is a REST API server that hosts a player willing to play games.
|
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. |
|
||||||
|
|
@ -15,7 +15,7 @@ def main() -> int:
|
||||||
:return: Exit code
|
:return: Exit code
|
||||||
:rtype: int
|
:rtype: int
|
||||||
"""
|
"""
|
||||||
player = PyServer(agents.RemoteAgent(url="https://bmt001.noordstar.me/"))
|
player = PyServer(agents.AgentOfChaos())
|
||||||
|
|
||||||
# Start listening for games
|
# Start listening for games
|
||||||
player.start(
|
player.start(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue