Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 36 additions & 22 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# This workflow will install Python dependencies, run tests, lint, and generate documentation
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application
name: Python Tests and Documentation

on:
push:
Expand All @@ -16,26 +16,40 @@ permissions:

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.8
uses: actions/setup-python@v3
with:
python-version: "3.8"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
PYTHONPATH=$(pwd) pytest tests -v --capture=sys
- uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Run tests
run: |
PYTHONPATH=${{ github.workspace }} pytest tests -v --capture=sys

- name: Generate documentation
run: |
PYTHONPATH=${{ github.workspace }} python docs.py

- name: Deploy documentation
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ share/python-wheels/
*.egg
MANIFEST

# Virtual environments
cuttle-bot/
cuttle-bot-3.12/
venv/
ENV/

# 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.
Expand Down Expand Up @@ -170,4 +176,8 @@ cython_debug/
test_games/
tmp.txt
game_history/
docs/
docs/
test_outputs/

# linters
.ruff_cache/
28 changes: 24 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
# Get the current working directory
CURRENT_DIR := $(shell pwd)

# Virtual environment name
VENV_NAME := cuttle-bot-3.12

# Add command to run tests
# --capture=tee-sys is used to capture the output of the tests and print it to the console
test:
PYTHONPATH=$(CURRENT_DIR) pytest tests -v --capture=tee-sys
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) pytest tests -v --capture=tee-sys

run:
PYTHONPATH=$(CURRENT_DIR) python main.py
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) python main.py

# Generate documentation using pdoc
docs:
PYTHONPATH=$(CURRENT_DIR) python docs.py
source $(VENV_NAME)/bin/activate && PYTHONPATH=$(CURRENT_DIR) python docs.py

# Clean generated documentation
clean-docs:
rm -rf docs/
rm -rf docs/

# Setup virtual environment
setup:
python3.12 -m venv $(VENV_NAME)
source $(VENV_NAME)/bin/activate && pip install -r requirements.txt

# Clean virtual environment
clean-venv:
rm -rf $(VENV_NAME)/

# Default target
all: test

# Type checking
typecheck:
@echo "Running mypy type checks..."
source $(VENV_NAME)/bin/activate && mypy .
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
## Create a virtual environment

```bash
python3 -m venv cuttle-bot
source ./cuttle-bot/bin/activate
python3 -m venv cuttle-bot-3.12
source ./cuttle-bot-3.12/bin/activate
```


Expand All @@ -33,7 +33,7 @@ The test output can be quite verbose, so it's recommended to redirect the output
`tmp.txt` is added to `.gitignore` to avoid polluting the repo with test output.

```bash
source ./cuttle-bot/bin/activate && make test > tmp.txt 2>&1
source ./cuttle-bot-3.12/bin/activate && make test > tmp.txt 2>&1
```

Or you can simply run `make test` to run the tests and see the output in the terminal.
Expand Down
18 changes: 9 additions & 9 deletions docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@
This script generates HTML documentation using pdoc.
"""

import os
import sys
import pdoc
from pathlib import Path

def generate_docs():

def generate_docs() -> None:
"""
Generate documentation for the project.
"""
# Define the output directory
output_dir = Path("docs")
output_dir.mkdir(exist_ok=True)

# Define the modules to document
modules = [
"game",
Expand All @@ -30,13 +29,13 @@ def generate_docs():
"game.utils",
"main",
]

# Generate documentation
pdoc.render.configure(
docformat="google", # Use Google-style docstrings
show_source=True, # Show source code
show_source=True, # Show source code
)

# Generate documentation for each module
for module in modules:
try:
Expand All @@ -47,9 +46,10 @@ def generate_docs():
print(f"Generated documentation for {module}")
except Exception as e:
print(f"Error generating documentation for {module}: {e}")

print(f"\nDocumentation generated in {output_dir.absolute()}")
print("You can view the documentation by opening docs/index.html in your browser")


if __name__ == "__main__":
generate_docs()
generate_docs()
36 changes: 25 additions & 11 deletions game/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

from __future__ import annotations

from game.card import Card
from enum import Enum
from typing import Optional

from game.card import Card


class ActionSource(Enum):
Expand Down Expand Up @@ -50,28 +52,28 @@ class Action:
"""

action_type: ActionType
card: Card
target: Card
card: Optional[Card]
target: Optional[Card]
played_by: int
requires_additional_input: bool
source: ActionSource

def __init__(
self,
action_type: ActionType,
card: Card,
target: Card,
played_by: int,
card: Optional[Card] = None,
target: Optional[Card] = None,
requires_additional_input: bool = False,
source: ActionSource = ActionSource.HAND,
):
"""Initialize a new Action instance.

Args:
action_type (ActionType): The type of action being performed.
card (Card): The card being played or used.
target (Card): The target card (if any) for the action.
played_by (int): Index of the player performing the action (0 or 1).
card (Optional[Card], optional): The card being played or used. Defaults to None.
target (Optional[Card], optional): The target card (if any) for the action. Defaults to None.
requires_additional_input (bool, optional): Whether more input is needed.
Defaults to False.
source (ActionSource, optional): Where the card comes from.
Expand Down Expand Up @@ -107,15 +109,27 @@ def __repr__(self) -> str:
elif self.action_type == ActionType.ONE_OFF:
return f"Play {self.card} as one-off"
elif self.action_type == ActionType.SCUTTLE:
return f"Scuttle {self.target} on P{self.target.played_by}'s field with {self.card}"
target_str = str(self.target) if self.target else "None"
card_str = str(self.card) if self.card else "None"
target_player = self.target.played_by if self.target else '?'
return f"Scuttle {target_str} on P{target_player}'s field with {card_str}"
elif self.action_type == ActionType.DRAW:
return "Draw a card from deck"
elif self.action_type == ActionType.COUNTER:
return f"Counter {self.target} with {self.card}"
target_str = str(self.target) if self.target else "None"
card_str = str(self.card) if self.card else "None"
return f"Counter {target_str} with {card_str}"
elif self.action_type == ActionType.JACK:
return f"Play {self.card} as jack on {self.target}"
target_str = str(self.target) if self.target else "None"
card_str = str(self.card) if self.card else "None"
return f"Play {card_str} as jack on {target_str}"
elif self.action_type == ActionType.RESOLVE:
return f"Resolve one-off {self.target}"
target_str = str(self.target) if self.target else "None"
return f"Resolve one-off {target_str}"
else:
# Handle any unexpected action types
card_str = str(self.card) if self.card else "None"
return f"Unknown Action: {self.action_type.value} with card {card_str}"

def __str__(self) -> str:
"""Get a string representation of the action.
Expand Down
Loading