Compare commits

..

2 Commits

Author SHA1 Message Date
Bram van den Heuvel 78c461d9b2 Create client script with diagnostics 2026-06-19 15:46:03 +02:00
Bram van den Heuvel eb5a869cf4 Containerize PyServer 2026-06-19 14:34:23 +02:00
8 changed files with 331 additions and 13 deletions

179
.dockerignore Normal file
View File

@ -0,0 +1,179 @@
# Ignore Markdown documentation
spec/
*.md
# ----------------------------
# ---> GITIGNORE CONFIGURATION
# ----------------------------
# Repository-specific virtual environments
.venv-pyserver
.venv-pyclient
# ---> Elm
# elm-package generated files
elm-stuff
# elm-repl generated files
repl-temp-*
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# Repository-specific virtual environments
.venv-pyserver
.venv-pyclient
# ---> Elm # ---> Elm
# elm-package generated files # elm-package generated files
elm-stuff elm-stuff

87
client.py Normal file
View File

@ -0,0 +1,87 @@
"""
This module offers a small place to start building game clients from.
You can use this for:
- Learning how the system works.
- Debugging a game server.
- Debugging a game that behaves weirdly.
This module lets you host a game, let online servers participate in it,
and then analyze the outcome.
You can build several things with this:
1. You can build an AI trainer that trains on existing players.
2. You can build an ELO evaluator that compares the performance of various
strategies.
"""
from __future__ import annotations
import json
from pyclient import PyClient
from pyclient.games.tic_tac_toe import TicTacToe
from typing import Any
def main() -> int:
"""
Start a client, then use it to analyze one or more matches.
:return: Exit code
:rtype: int
"""
c = PyClient([
"https://bmt001.noordstar.me",
"https://bmt001.noordstar.me",
], debug=False)
out = c.play_game("tic-tac-toe", TicTacToe)
inspect_game(out)
return 0
def inspect_game(game : list[tuple[int, dict[str, Any], Any]]) -> None:
"""
Print a diagnostic of a played game to the terminal.
:param game: The results of a played game.
:type game: list[typle[int, dict[str, Any], Any]]
"""
max_width = 40
hbar = max_width * '%'
def title(s : str) -> None:
"""
Show a title nice and clean in the middle
:param s: The title to display
:type s: str
"""
w = (max_width - len(s)) // 2
print(hbar)
print((w * ' ') + s.upper() + (w * ' '))
print(hbar)
# Show all moves made throughout the game
title("Turns taken")
for player, action, _ in game:
print(f"Player {player} : " + json.dumps(action)[:max_width-12])
# Show all remaining variables in the finishing state of the game
title("Final state")
final_state = game[-1][2]
for k, v in final_state.to_dict().items():
print(f"{k} => {json.dumps(v)}")
# Some final (usually the most relevant) statistics
title("Result")
print(f"Total turns taken: {len(game)}")
print("Winner: " + ("0 (tie)" if final_state.winner() == 0 else f"player {final_state.winner()}"))
if __name__ == "__main__":
raise SystemExit(main())

25
pyserver/Containerfile Normal file
View File

@ -0,0 +1,25 @@
FROM python:3.10-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache gcc musl-dev python3-dev
COPY requirements-pyserver.txt .
# Create wheels for faster installation
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements-pyserver.txt
FROM python:3.10-alpine
WORKDIR /app
# Install from pre-built wheels
COPY --from=builder /wheels /wheels
COPY requirements-pyserver.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels \
-r requirements-pyserver.txt && rm -rf /wheels
# Install PyServer code
COPY pyserver/ pyserver/
COPY server.py .
CMD ["python", "server.py"]

View File

@ -0,0 +1,5 @@
certifi==2026.6.17
charset-normalizer==3.4.7
idna==3.18
requests==2.34.2
urllib3==2.7.0

View File

@ -0,0 +1,8 @@
blinker==1.9.0
click==8.4.1
colorama==0.4.6
Flask==3.1.3
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
Werkzeug==3.1.8

View File

@ -1,5 +1,5 @@
blinker==1.9.0 blinker==1.9.0
certifi==2026.5.20 certifi==2026.6.17
charset-normalizer==3.4.7 charset-normalizer==3.4.7
click==8.4.1 click==8.4.1
colorama==0.4.6 colorama==0.4.6

View File

@ -2,13 +2,20 @@
This module enables a user to host a server that is able to play games. This module enables a user to host a server that is able to play games.
""" """
from __future__ import annotations
import random import random
from pyserver import PyServer from pyserver import PyServer
from pyclient import PyClient from typing import Any
from pyclient.games.tic_tac_toe import TicTacToe
def main(): def main() -> int:
"""
Start a server.
:return: Exit code
:rtype: int
"""
player = PyServer( player = PyServer(
# Customize this to whatever you'd like to call your player # Customize this to whatever you'd like to call your player
name="My super smart robot player", name="My super smart robot player",
@ -20,13 +27,18 @@ def main():
import_name=__name__, import_name=__name__,
) )
# Register games! Comment out any you don't want your player to play.
player.add_tic_tac_toe(on_move=play_tic_tac_toe, profile={}) player.add_tic_tac_toe(on_move=play_tic_tac_toe, profile={})
player.start(port=5001) # Start listening for games
player.start(
host="0.0.0.0", # Comment out when using only locally
port=5000,
)
return 0 return 0
def play_tic_tac_toe(payload): def play_tic_tac_toe(payload : dict[str, Any]) -> dict[str, Any]:
""" """
Play a game of tic-tac-toe. Play a game of tic-tac-toe.
@ -52,6 +64,11 @@ def play_tic_tac_toe(payload):
4 | 5 | 6 4 | 5 | 6
---+---+--- ---+---+---
7 | 8 | 9 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! # Try printing the payload to see what it looks like!
@ -80,10 +97,3 @@ def play_tic_tac_toe(payload):
if __name__ == "__main__": if __name__ == "__main__":
raise SystemExit(main()) raise SystemExit(main())
c = PyClient([
"http://127.0.0.1:5001",
"http://127.0.0.1:5002",
], debug=True)
out = c.play_game("tic-tac-toe", TicTacToe)