diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e77097f --- /dev/null +++ b/.dockerignore @@ -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/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index a9936b6..0b72478 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Repository-specific virtual environments +.venv-pyserver +.venv-pyclient + # ---> Elm # elm-package generated files elm-stuff diff --git a/pyserver/Containerfile b/pyserver/Containerfile new file mode 100644 index 0000000..c9d100d --- /dev/null +++ b/pyserver/Containerfile @@ -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"] diff --git a/requirements-pyserver.txt b/requirements-pyserver.txt new file mode 100644 index 0000000..c2d75db --- /dev/null +++ b/requirements-pyserver.txt @@ -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 diff --git a/server.py b/server.py new file mode 100644 index 0000000..bf95a28 --- /dev/null +++ b/server.py @@ -0,0 +1,99 @@ +""" + This module enables a user to host a server that is able to play games. +""" + +from __future__ import annotations + +import random + +from pyserver import PyServer +from typing import Any + +def main() -> int: + """ + Start a server. + + :return: Exit code + :rtype: int + """ + player = PyServer( + # Customize this to whatever you'd like to call your player + name="My super smart robot player", + + # Custom information that you can use to tell people about this player + profile={}, + + # Unless you know what you're doing, don't touch this. + 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={}) + + # Start listening for games + player.start( + host="0.0.0.0", # Comment out when using only locally + port=5000, + ) + + return 0 + +def play_tic_tac_toe(payload : dict[str, Any]) -> dict[str, Any]: + """ + 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 logic. + return { "move": random.choice(options) } + +if __name__ == "__main__": + raise SystemExit(main())