Skip to content

Commit

Permalink
Create cards table, and support cards parsing (#377)
Browse files Browse the repository at this point in the history
- **Create cards table migration**
- **Create cards model**
- **In BGA log parser, parse card plays in each move**
- **Raise exception if move not set**
- **Create card plays table & model, and add migration to populate data
from existing logs**
- **Add TODO**
- **Bind dev postgres, and delete unused imports**
- **Remove game cards migration**
  • Loading branch information
shaldengeki authored Aug 13, 2024
1 parent 6316117 commit ab3337a
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"""

import json
import logging

import sqlalchemy as sa
from alembic import op
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""create-cards-table
Revision ID: dc8331e9955b
Revises: fef90e3dfd97
Create Date: 2024-08-12 16:11:42.008947
"""

import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql.functions import now

# revision identifiers, used by Alembic.
revision = "dc8331e9955b"
down_revision = "fef90e3dfd97"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"cards",
sa.Column("id", sa.Integer, primary_key=True, autoincrement=True),
sa.Column("name", sa.UnicodeText, nullable=False),
sa.Column("bga_id", sa.UnicodeText, nullable=False, unique=True),
sa.Column("created_at", sa.DateTime, default=now, nullable=False),
)

op.create_index(
"cards_bga_id",
"cards",
["bga_id"],
unique=True,
)


def downgrade():
op.drop_index("cards_bga_id", "cards")
op.drop_table("cards")
8 changes: 8 additions & 0 deletions ark_nova_stats/bga_log_parser/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@ class BGALogParserError(BaseException):
pass


class MoveNotSetError(BGALogParserError):
pass


class NonArkNovaReplayError(BGALogParserError):
pass


class PlayerNotFoundError(BGALogParserError):
pass
68 changes: 63 additions & 5 deletions ark_nova_stats/bga_log_parser/fixtures/play_event.log.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,69 @@
"uid": "669cd3629ee68",
"type": "buyAnimal",
"log": "${player_name} plays ${card_name} for ${amount_money} and places it in ${building_name}",
"args": {"card": {"id": "A445_CrestedPorcupine", "location": "inPlay", "state": 19, "pId": 94929538, "extraDatas": null}, "amount": 8, "amount_money": 8, "total": 5, "building": {"id": 8, "location": "board", "state": 1, "pId": 94929538, "type": "size-1", "x": 1, "y": 8, "rotation": 0}, "icons": {"Bird": 1, "Predator": 1, "Herbivore": 1, "Bear": 1, "Reptile": 1, "Pet": 0, "Primate": 0, "Africa": 0, "Europe": 1, "Asia": 1, "Americas": 1, "Australia": 0, "Partner-Zoo": 0, "AnimalsII": 0, "CardsII": 0, "Science": 1, "Fac": 0, "Rock": 2, "Water": 2, "SeaAnimal": 0}, "fromDisplay": false, "player_name": "Duci9", "player_id": 94929538, "i18n": ["building_name", "card_name"], "building_name": {"log": "a size-${n} enclosure", "args": {"n": 1}},
"card_id": "A445_CrestedPorcupine",
"card_name": "Crested Porcupine",
"preserve": ["card_id"]},
"args": {
"card": {
"id": "A445_CrestedPorcupine",
"location": "inPlay",
"state": 19,
"pId": 94929538,
"extraDatas": null
},
"amount": 8,
"amount_money": 8,
"total": 5,
"building": {
"id": 8,
"location": "board",
"state": 1,
"pId": 94929538,
"type": "size-1",
"x": 1,
"y": 8,
"rotation": 0
},
"icons": {
"Bird": 1,
"Predator": 1,
"Herbivore": 1,
"Bear": 1,
"Reptile": 1,
"Pet": 0,
"Primate": 0,
"Africa": 0,
"Europe": 1,
"Asia": 1,
"Americas": 1,
"Australia": 0,
"Partner-Zoo": 0,
"AnimalsII": 0,
"CardsII": 0,
"Science": 1,
"Fac": 0,
"Rock": 2,
"Water": 2,
"SeaAnimal": 0
},
"fromDisplay": false,
"player_name": "Duci9",
"player_id": 94929538,
"i18n": [
"building_name",
"card_name"
],
"building_name": {
"log": "a size-${n} enclosure",
"args": {
"n": 1
}
},
"card_id": "A445_CrestedPorcupine",
"card_name": "Crested Porcupine",
"preserve": [
"card_id"
]
},
"lock_uuid": "one",
"synchro": "one",
"h": "08137c"
}
}
69 changes: 67 additions & 2 deletions ark_nova_stats/bga_log_parser/game_log.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from dataclasses import dataclass
from typing import Optional
from typing import Iterator, Optional

from ark_nova_stats.bga_log_parser.exceptions import NonArkNovaReplayError
from ark_nova_stats.bga_log_parser.exceptions import (
MoveNotSetError,
NonArkNovaReplayError,
PlayerNotFoundError,
)


@dataclass
class GameLogEventDataCard:
name: str
id: str


@dataclass
Expand Down Expand Up @@ -43,6 +53,30 @@ def played_card_names(self) -> Optional[set[str]]:

return card_names

@property
def played_cards(self) -> Optional[list[GameLogEventDataCard]]:
cards = []
if "card_name" in self.args:
cards = [
GameLogEventDataCard(
name=self.args["card_name"], id=self.args["card_id"]
)
]
elif "card_names" in self.args:
# potentially multiple cards are played in this action.
for arg_key, arg_val in self.args["card_names"]["args"].items():
if "args" in arg_val and "card_name" in arg_val["args"]:
cards.append(
GameLogEventDataCard(
name=arg_val["args"]["card_name"],
id=arg_val["args"]["card_id"],
)
)
else:
return None

return cards

@property
def player(self) -> Optional[dict[str, int | str]]:
player_data = {
Expand Down Expand Up @@ -82,6 +116,13 @@ class GameLogPlayer:
avatar: str


@dataclass
class GameLogCardPlay:
card: GameLogEventDataCard
player: GameLogPlayer
move: int


@dataclass
class GameLogData:
logs: list[GameLogEvent]
Expand All @@ -107,6 +148,30 @@ def validate_is_ark_nova_game(self) -> None:
):
raise NonArkNovaReplayError()

@property
def card_plays(self) -> Iterator[GameLogCardPlay]:
for log in self.logs:
for d in log.data:
if d.played_cards is None:
continue

for c in d.played_cards:
if log.move_id is None:
raise MoveNotSetError()

if d.player is None:
raise PlayerNotFoundError()

find_player = [p for p in self.players if p.id == d.player["id"]]
if not find_player:
raise PlayerNotFoundError()

yield GameLogCardPlay(
card=c,
move=log.move_id,
player=find_player[0],
)


@dataclass
class GameLog:
Expand Down
25 changes: 25 additions & 0 deletions ark_nova_stats/bga_log_parser/game_log_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ def test_parses_4p_game(self):
game_log = load_data_from_fixture_file("4p.log.json")
GameLog(**game_log)

def test_card_plays_for_4p_game(self):
game_log = load_data_from_fixture_file("4p.log.json")
plays = list(GameLog(**game_log).data.card_plays)
assert 58 == len(plays)
assert "Baboon Rock" == plays[0].card.name
assert "Victory Column" == plays[-1].card.name


class TestGameLogEventData:
def test_is_play_event_returns_true_for_play_action(self):
Expand Down Expand Up @@ -74,6 +81,24 @@ def test_played_card_names_for_new_conservation_project(self):
x = GameLogEventData(**play_log)
assert set(["Yosemite national park"]) == x.played_card_names

def test_played_card_for_play_action(self):
play_log = load_data_from_fixture_file("play_event.log.json")
x = GameLogEventData(**play_log)
cards = x.played_cards
assert cards is not None
assert 1 == len(cards)
assert cards[0].id == "A445_CrestedPorcupine"
assert cards[0].name == "Crested Porcupine"

def test_played_card_for_new_conservation_project(self):
play_log = load_data_from_fixture_file("play_new_conservation_project.log.json")
x = GameLogEventData(**play_log)
cards = x.played_cards
assert cards is not None
assert 1 == len(cards)
assert cards[0].id == "P114_ReleaseYosemite"
assert cards[0].name == "Yosemite national park"


if __name__ == "__main__":
sys.exit(pytest.main([__file__] + sys.argv[1:]))
2 changes: 2 additions & 0 deletions ark_nova_stats/docker-compose.override.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ services:
pg:
env_file:
- env/.postgres.env
ports:
- "5432:5432"
frontend:
image: shaldengeki/ark-nova-stats-frontend:latest
env_file:
Expand Down
54 changes: 54 additions & 0 deletions ark_nova_stats/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ class GameLog(db.Model): # type: ignore
back_populates="last_game_log",
)

# cards: Mapped[list["Card"]] = relationship(
# secondary="game_log_cards", back_populates="game_logs", viewonly=True
# )

# card_plays: Mapped[list["CardPlay"]] = relationship(back_populates="game_log")

def create_related_objects(self, parsed_logs: ParsedGameLog) -> db.Model: # type: ignore
# Add users if not present.
present_users = User.query.filter(
Expand Down Expand Up @@ -77,6 +83,8 @@ def create_related_objects(self, parsed_logs: ParsedGameLog) -> db.Model: # typ
game_log=self,
)

# TODO: create card & card play models.


class User(db.Model): # type: ignore
__tablename__ = "users"
Expand All @@ -96,6 +104,11 @@ class User(db.Model): # type: ignore
game_participations: Mapped[list["GameParticipation"]] = relationship(
back_populates="user"
)
# card_plays: Mapped[list["CardPlay"]] = relationship(back_populates="user")

# cards: Mapped[list["Card"]] = relationship(
# secondary="game_log_cards", back_populates="users", viewonly=True
# )

@property
def num_game_logs(self) -> int:
Expand Down Expand Up @@ -139,3 +152,44 @@ class GameLogArchive(db.Model): # type: ignore
)

last_game_log: Mapped[GameLog] = relationship(back_populates="archives")


class Card(db.Model): # type: ignore
__tablename__ = "cards"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
bga_id: Mapped[str] = mapped_column(unique=True)
created_at: Mapped[datetime.datetime] = mapped_column(
db.TIMESTAMP(timezone=True),
default=lambda: datetime.datetime.now(tz=datetime.timezone.utc),
)

# plays: Mapped[list["CardPlay"]] = relationship(back_populates="card")

# game_logs: Mapped[list["GameLog"]] = relationship(
# secondary="game_log_cards", back_populates="cards", viewonly=True
# )

# users: Mapped[list["User"]] = relationship(
# secondary="game_log_cards", back_populates="cards", viewonly=True
# )


# class CardPlay(db.Model): # type: ignore
# __tablename__ = "game_log_cards"

# game_log_id: Mapped[int] = mapped_column(
# ForeignKey("game_logs.id"), primary_key=True
# )
# card_id: Mapped[int] = mapped_column(ForeignKey("cards.id"), primary_key=True)
# user_id: Mapped[int] = mapped_column(ForeignKey("users.bga_id"), primary_key=True)
# move: Mapped[int]
# created_at: Mapped[datetime.datetime] = mapped_column(
# db.TIMESTAMP(timezone=True),
# default=lambda: datetime.datetime.now(tz=datetime.timezone.utc),
# )

# user: Mapped["User"] = relationship(back_populates="card_plays")
# game_log: Mapped["GameLog"] = relationship(back_populates="card_plays")
# card: Mapped["Card"] = relationship(back_populates="plays")

0 comments on commit ab3337a

Please sign in to comment.