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
39 changes: 39 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v3
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: Test with pytest
run: |
pytest
98 changes: 94 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[tool.black]
line-length = 88
skip-string-normalization = true
target-version = ["py312"]

[tool.poetry]
name = "power-flow-game"
version = "0.0.0"
Expand All @@ -10,3 +15,6 @@ python = "3.12.*"
pypsa = "^0.34.1"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do we need pypsa for? and how about an optimization module such as pyomo or linopy?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These packages are just placeholders for now to get going. We can change them when we start actually using packages

pandas = "^2.2.3"
numpy = "^2.2.5"

[tool.poetry.group.dev.dependencies]
black = "^25.1.0"
17 changes: 17 additions & 0 deletions src/engine/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from src.models.event import Event
from src.models.game_state import GameState


class Engine:
def __init__(self, game_state: GameState) -> None:
self.game_state = game_state

def handle_event(self, event: Event) -> None:
"""
Events happen every time a player takes an action or a timer runs out.
Every time an event occurs, the engine is informed and it can then:
-Update the game state
-Send messages back to the player interface if required
:param event:
"""
raise NotImplementedError()
7 changes: 7 additions & 0 deletions src/models/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from abc import ABC
from dataclasses import dataclass


@dataclass(frozen=True)
class Event(ABC):
pass
72 changes: 72 additions & 0 deletions src/models/game_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from typing import Self, Optional
import json

from src.models.player import Player, PlayerId


class Phase(Enum):
# Values are just placeholders
CONSTRUCTION = 0
SNEAKY_TRICKS = 1
DA_AUCTION = 2


@dataclass
class GameState:
# A complete description of the current state of the game.
def __init__(
self,
game_id: int,
players: list[Player],
phase: Phase,
current_player: Optional[PlayerId],
) -> None:
# Read only attributes
self._game_id = game_id
self._players = players

# Mutable attributes
self.phase = phase
self.current_player = current_player

@property
def game_id(self) -> int:
return self._game_id

@property
def players(self) -> list[Player]:
return self._players

@cached_property
def n_players(self) -> int:
return len(self.players)

def to_simple_dict(self) -> dict:
return {
"game_id": self.game_id,
"players": [player.to_simple_dict() for player in self.players],
"phase": self.phase.value,
"current_player": (
self.current_player.as_int()
if self.current_player is not None
else None
),
}

@classmethod
def from_simple_dict(cls, simple_dict: dict) -> Self:
return cls(
game_id=simple_dict["game_id"],
players=[
Player.from_simple_dict(player) for player in simple_dict["players"]
],
phase=Phase(simple_dict["phase"]),
current_player=(
PlayerId(simple_dict["current_player"])
if simple_dict["current_player"] is not None
else None
),
)
23 changes: 23 additions & 0 deletions src/models/player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass
from typing import Self


class PlayerId(int):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of the PlayerId class?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The player id is a concept that we will use a lot in the project so by wrapping it in a class and giving it a name it will be easy to know exactly what we are referring to.

Sometimes types allow you to describe the meaning of things directly for example:
bids: dict[PlayerId, Bid]
Vs
bids: dict[int, Bid] # keys are player IDs

Also if you accidentally pass some other integer or id the type checker will complain

Also if we ever decide to change the player id to be uuid or str or something we would only have to change it in one place

def as_int(self) -> int:
return int(self)


@dataclass(frozen=True)
class Player:
id: PlayerId
name: str

def to_simple_dict(self) -> dict:
return {"id": self.id.as_int(), "name": self.name}

@classmethod
def from_simple_dict(cls, simple_dict: dict) -> Self:
return cls(
id=PlayerId(simple_dict["id"]),
name=simple_dict["name"],
)
Empty file added src/tools/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions src/tools/serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Protocol, Self, runtime_checkable, TypeVar
import json


@runtime_checkable
class Serializable(Protocol):
def to_simple_dict(self) -> dict: ...

@classmethod
def from_simple_dict(cls, simple_dict: dict) -> Self: ...


GenericSerializable = TypeVar("GenericSerializable", bound=Serializable)


def serialize(x: Serializable) -> str:
return json.dumps(x.to_simple_dict())


def deserialize(x: str, cls: type[GenericSerializable]) -> GenericSerializable:
return cls.from_simple_dict(json.loads(x))
5 changes: 5 additions & 0 deletions src/tools/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import TypeVar

T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
Empty file added tests/test_models/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions tests/test_models/test_game_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from unittest import TestCase

from src.models.game_state import GameState, Phase
from src.models.player import Player, PlayerId
from src.tools.serialization import serialize, deserialize


class TestGameState(TestCase):
def test_serialization(self) -> None:
# Test the serialization of the GameState object
game_state = GameState(
game_id=1,
players=[
Player(id=PlayerId(1), name="Alice"),
Player(id=PlayerId(2), name="Bob"),
],
phase=Phase.CONSTRUCTION,
current_player=PlayerId(1),
)
json_str = serialize(game_state)
re_built_state = deserialize(x=json_str, cls=GameState)
self.assertEqual(game_state.game_id, re_built_state.game_id)
for (
p1,
p2,
) in zip(game_state.players, re_built_state.players):
Comment thread
RobbieKiwi marked this conversation as resolved.
self.assertEqual(p1, p2)
self.assertEqual(game_state.phase, re_built_state.phase)