Skip to content

Commit

Permalink
In BGA log parser, parse stats as well (#403)
Browse files Browse the repository at this point in the history
- **Add stats.proto and unit test for simple game stats**
- **Load the right fixture for the test**
- **Add logic to parse player stats**
- **Remove print**
- **Search for stats event instead of looking at the very end**
- **Make skipping a little faster**
- **Don't print**
- **Add comment**
- **Make win/tie search a little more resilient**
- **Simplify logic**
- **Simplify a little more**
- **Add script to fix emu cup data**
- **Fix user names in worker script**
  • Loading branch information
shaldengeki authored Aug 20, 2024
1 parent 0038e11 commit a6454a4
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 18 deletions.
1 change: 1 addition & 0 deletions ark_nova_stats/bga_log_parser/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ py_library(
deps = [
":exceptions",
"//ark_nova_stats/bga_log_parser/proto:game_proto_py_pb2",
"//ark_nova_stats/bga_log_parser/proto:stats_proto_py_pb2",
],
)

Expand Down
4 changes: 4 additions & 0 deletions ark_nova_stats/bga_log_parser/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class MoveNotSetError(BGALogParserError):
pass


class StatsNotSetError(BGALogParserError):
pass


class NonArkNovaReplayError(BGALogParserError):
pass

Expand Down

Large diffs are not rendered by default.

138 changes: 125 additions & 13 deletions ark_nova_stats/bga_log_parser/game_log.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from dataclasses import dataclass
from typing import Iterator, Optional
from typing import Any, Iterator, Optional

from ark_nova_stats.bga_log_parser.exceptions import (
MoveNotSetError,
NonArkNovaReplayError,
PlayerNotFoundError,
StatsNotSetError,
)
from ark_nova_stats.bga_log_parser.proto.game_pb2 import Game
from ark_nova_stats.bga_log_parser.proto.stats_pb2 import PlayerStats, Stats


@dataclass
Expand Down Expand Up @@ -183,6 +185,7 @@ def __post_init__(self):
self.data = GameLogData(**self.data) # type: ignore
# TODO: populate this from self.data.
self.game = Game()
self.stats = self.parse_game_stats()

@property
def table_id(self) -> Optional[int]:
Expand All @@ -196,14 +199,18 @@ def winner(self) -> Optional[GameLogPlayer]:
if not self.data.logs:
return None

# Look at the last move.
last_move = self.data.logs[-1]
try:
victory_event = next(
e for e in last_move.data if e.type == "simpleNode" and "wins!" in e.log
)
except StopIteration:
# No winner.
# Look at the last few moves.
victory_event = None
for move in self.data.logs[-10:]:
for e in move.data:
if e.type == "simpleNode" and "wins" in e.log:
victory_event = e
break

if victory_event is not None:
break

if victory_event is None:
return None

return next(
Expand All @@ -215,10 +222,115 @@ def is_tie(self) -> bool:
if not self.data.logs:
return False

# Look at the last move.
last_move = self.data.logs[-1]

# Look at the last few moves.
return any(
e.type == "simpleNode" and "End of game (tie)" in e.log
for e in last_move.data
for move in self.data.logs[-10:]
for e in move.data
)

def parse_player_stats(self, stats: dict[str, Any]) -> PlayerStats:
# The int keys here come from BGA's own format in replays;
# I expect these to change / break over time as BGA changes its own format.
# TODO: look into .get() instead; maybe we can set these to optional to make it more resilient?
return PlayerStats(
player_id=int(stats["player"]),
score=int(stats["score"]),
rank=int(stats["rank"]),
thinking_time=int(stats["stats"]["1"]),
starting_position=int(stats["stats"]["11"]),
turns=int(stats["stats"]["12"]),
breaks_triggered=int(stats["stats"]["13"]),
triggered_end=bool(int(stats["stats"]["14"])),
map_id=int(stats["stats"]["15"]),
appeal=int(stats["stats"]["16"]),
conservation=int(stats["stats"]["17"]),
reputation=int(stats["stats"]["19"]),
actions_build=int(stats["stats"]["20"]),
actions_animals=int(stats["stats"]["21"]),
actions_cards=int(stats["stats"]["22"]),
actions_association=int(stats["stats"]["23"]),
actions_sponsors=int(stats["stats"]["24"]),
x_tokens_gained=int(stats["stats"]["25"]),
x_actions=int(stats["stats"]["26"]),
x_tokens_used=int(stats["stats"]["27"]),
money_gained=int(stats["stats"]["30"]),
money_gained_through_income=int(stats["stats"]["31"]),
money_spent_on_animals=int(stats["stats"]["32"]),
money_spent_on_enclosures=int(stats["stats"]["33"]),
money_spent_on_donations=int(stats["stats"]["34"]),
money_spent_on_playing_cards_from_reputation_range=int(
stats["stats"]["35"]
),
cards_drawn_from_deck=int(stats["stats"]["40"]),
cards_drawn_from_reputation_range=int(stats["stats"]["41"]),
cards_snapped=int(stats["stats"]["42"]),
cards_discarded=int(stats["stats"]["43"]),
played_sponsors=int(stats["stats"]["44"]),
played_animals=int(stats["stats"]["45"]),
release_animals=int(stats["stats"]["46"]),
association_workers=int(stats["stats"]["50"]),
association_donations=int(stats["stats"]["51"]),
association_reputation_actions=int(stats["stats"]["52"]),
association_partner_zoo_actions=int(stats["stats"]["53"]),
association_university_actions=int(stats["stats"]["54"]),
association_conservation_project_actions=int(stats["stats"]["55"]),
built_enclosures=int(stats["stats"]["60"]),
built_kiosks=int(stats["stats"]["61"]),
built_pavilions=int(stats["stats"]["62"]),
built_unique_buildings=int(stats["stats"]["63"]),
hexes_covered=int(stats["stats"]["64"]),
hexes_empty=int(stats["stats"]["65"]),
upgraded_action_cards=int(stats["stats"]["70"]),
upgraded_animals=bool(int(stats["stats"]["71"])),
upgraded_build=bool(int(stats["stats"]["72"])),
upgraded_cards=bool(int(stats["stats"]["73"])),
upgraded_sponsors=bool(int(stats["stats"]["74"])),
upgraded_association=bool(int(stats["stats"]["75"])),
icons_africa=int(stats["stats"]["76"]),
icons_europe=int(stats["stats"]["77"]),
icons_asia=int(stats["stats"]["78"]),
icons_australia=int(stats["stats"]["79"]),
icons_americas=int(stats["stats"]["80"]),
icons_bird=int(stats["stats"]["81"]),
icons_predator=int(stats["stats"]["82"]),
icons_herbivore=int(stats["stats"]["83"]),
icons_bear=int(stats["stats"]["84"]),
icons_reptile=int(stats["stats"]["85"]),
icons_primate=int(stats["stats"]["86"]),
icons_petting_zoo=int(stats["stats"]["97"]),
icons_sea_animal=int(stats["stats"]["91"]),
icons_water=int(stats["stats"]["88"]),
icons_rock=int(stats["stats"]["89"]),
icons_science=int(stats["stats"]["90"]),
)

def parse_game_stats(self) -> Stats:
# Player stats are in last event.
if not self.data.logs:
raise StatsNotSetError()

# The stats are in a log that's close to the end.
# It doesn't get deterministically emitted in any given position,
# so we search for it.
stats = None
for l in self.data.logs[-10:]:
for e in l.data:
if (
e.args
and e.args.get("args", {})
and e.args.get("args", {}).get("result", {})
):
stats = e.args["args"]["result"]
break
if stats is not None:
break

if stats is None:
raise StatsNotSetError()

return Stats(
player_stats=[
self.parse_player_stats(player_stats) for player_stats in stats
]
)
145 changes: 145 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 @@ -60,6 +60,151 @@ def test_card_plays_for_4p_game(self):
card_ids = [play.card.id for play in plays]
assert len(set(card_ids)) == len(card_ids)

def test_parses_game_stats(self):
game_log = load_data_from_fixture_file("533468391_darcelmaw_hardyzhao.json")
x = GameLog(**game_log)

stats = x.stats
assert 2 == len(stats.player_stats)

darcelmaw = stats.player_stats[0]
assert 93481498 == darcelmaw.player_id
assert 123 == darcelmaw.score
assert 1 == darcelmaw.rank
assert 769 == darcelmaw.thinking_time
assert 1 == darcelmaw.starting_position
assert 32 == darcelmaw.turns
assert 1 == darcelmaw.breaks_triggered
assert darcelmaw.triggered_end
assert 5 == darcelmaw.map_id
assert 72 == darcelmaw.appeal
assert 25 == darcelmaw.conservation
assert 9 == darcelmaw.reputation
assert 6 == darcelmaw.actions_build
assert 6 == darcelmaw.actions_animals
assert 4 == darcelmaw.actions_cards
assert 8 == darcelmaw.actions_association
assert 8 == darcelmaw.actions_sponsors
assert 7 == darcelmaw.x_tokens_gained
assert 1 == darcelmaw.x_actions
assert 4 == darcelmaw.x_tokens_used
assert 190 == darcelmaw.money_gained
assert 147 == darcelmaw.money_gained_through_income
assert 100 == darcelmaw.money_spent_on_animals
assert 44 == darcelmaw.money_spent_on_enclosures
assert 0 == darcelmaw.money_spent_on_donations
assert 0 == darcelmaw.money_spent_on_playing_cards_from_reputation_range
assert 3 == darcelmaw.cards_drawn_from_deck
assert 5 == darcelmaw.cards_drawn_from_reputation_range
assert 8 == darcelmaw.cards_snapped
assert 1 == darcelmaw.cards_discarded
assert 7 == darcelmaw.played_sponsors
assert 9 == darcelmaw.played_animals
assert 2 == darcelmaw.release_animals
assert 4 == darcelmaw.association_workers
assert 0 == darcelmaw.association_donations
assert 1 == darcelmaw.association_reputation_actions
assert 1 == darcelmaw.association_partner_zoo_actions
assert 2 == darcelmaw.association_university_actions
assert 4 == darcelmaw.association_conservation_project_actions
assert 9 == darcelmaw.built_enclosures
assert 2 == darcelmaw.built_kiosks
assert 6 == darcelmaw.built_pavilions
assert 3 == darcelmaw.built_unique_buildings
assert 43 == darcelmaw.hexes_covered
assert 0 == darcelmaw.hexes_empty
assert 3 == darcelmaw.upgraded_action_cards
assert darcelmaw.upgraded_animals
assert darcelmaw.upgraded_build
assert not darcelmaw.upgraded_cards
assert darcelmaw.upgraded_sponsors
assert not darcelmaw.upgraded_association
assert 0 == darcelmaw.icons_africa
assert 2 == darcelmaw.icons_europe
assert 5 == darcelmaw.icons_asia
assert 0 == darcelmaw.icons_australia
assert 1 == darcelmaw.icons_americas
assert 1 == darcelmaw.icons_bird
assert 1 == darcelmaw.icons_predator
assert 1 == darcelmaw.icons_herbivore
assert 0 == darcelmaw.icons_bear
assert 3 == darcelmaw.icons_reptile
assert 1 == darcelmaw.icons_primate
assert 1 == darcelmaw.icons_petting_zoo
assert 0 == darcelmaw.icons_sea_animal
assert 6 == darcelmaw.icons_water
assert 4 == darcelmaw.icons_rock
assert 2 == darcelmaw.icons_science

hardyzhao = stats.player_stats[1]
assert 92147740 == hardyzhao.player_id
assert 118 == hardyzhao.score
assert 2 == hardyzhao.rank
assert 1430 == hardyzhao.thinking_time
assert 2 == hardyzhao.starting_position
assert 32 == hardyzhao.turns
assert 4 == hardyzhao.breaks_triggered
assert not hardyzhao.triggered_end
assert 5 == hardyzhao.map_id
assert 76 == hardyzhao.appeal
assert 22 == hardyzhao.conservation
assert 15 == hardyzhao.reputation
assert 7 == hardyzhao.actions_build
assert 6 == hardyzhao.actions_animals
assert 5 == hardyzhao.actions_cards
assert 7 == hardyzhao.actions_association
assert 7 == hardyzhao.actions_sponsors
assert 9 == hardyzhao.x_tokens_gained
assert 1 == hardyzhao.x_actions
assert 4 == hardyzhao.x_tokens_used
assert 204 == hardyzhao.money_gained
assert 130 == hardyzhao.money_gained_through_income
assert 114 == hardyzhao.money_spent_on_animals
assert 60 == hardyzhao.money_spent_on_enclosures
assert 0 == hardyzhao.money_spent_on_donations
assert 0 == hardyzhao.money_spent_on_playing_cards_from_reputation_range
assert 19 == hardyzhao.cards_drawn_from_deck
assert 6 == hardyzhao.cards_drawn_from_reputation_range
assert 4 == hardyzhao.cards_snapped
assert 2 == hardyzhao.cards_discarded
assert 4 == hardyzhao.played_sponsors
assert 12 == hardyzhao.played_animals
assert 1 == hardyzhao.release_animals
assert 3 == hardyzhao.association_workers
assert 0 == hardyzhao.association_donations
assert 0 == hardyzhao.association_reputation_actions
assert 2 == hardyzhao.association_partner_zoo_actions
assert 0 == hardyzhao.association_university_actions
assert 5 == hardyzhao.association_conservation_project_actions
assert 12 == hardyzhao.built_enclosures
assert 4 == hardyzhao.built_kiosks
assert 2 == hardyzhao.built_pavilions
assert 0 == hardyzhao.built_unique_buildings
assert 34 == hardyzhao.hexes_covered
assert 9 == hardyzhao.hexes_empty
assert 3 == hardyzhao.upgraded_action_cards
assert hardyzhao.upgraded_animals
assert hardyzhao.upgraded_build
assert hardyzhao.upgraded_cards
assert not hardyzhao.upgraded_sponsors
assert not hardyzhao.upgraded_association
assert 9 == hardyzhao.icons_africa
assert 4 == hardyzhao.icons_europe
assert 0 == hardyzhao.icons_asia
assert 0 == hardyzhao.icons_australia
assert 1 == hardyzhao.icons_americas
assert 1 == hardyzhao.icons_bird
assert 4 == hardyzhao.icons_predator
assert 1 == hardyzhao.icons_herbivore
assert 0 == hardyzhao.icons_bear
assert 2 == hardyzhao.icons_reptile
assert 4 == hardyzhao.icons_primate
assert 0 == hardyzhao.icons_petting_zoo
assert 0 == hardyzhao.icons_sea_animal
assert 1 == hardyzhao.icons_water
assert 2 == hardyzhao.icons_rock
assert 3 == hardyzhao.icons_science


class TestGameLogEventData:
def test_is_play_event_returns_true_for_play_action(self):
Expand Down
18 changes: 18 additions & 0 deletions ark_nova_stats/bga_log_parser/proto/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ py_proto_library(
deps = [":game_proto"],
)

py_proto_library(
name = "stats_proto_py_pb2",
visibility = ["//visibility:public"],
deps = [":stats_proto"],
)

proto_library(
name = "animals_proto",
srcs = ["animals.proto"],
Expand Down Expand Up @@ -62,3 +68,15 @@ proto_library(
":sponsors_proto",
],
)

proto_library(
name = "map_proto",
srcs = ["map.proto"],
visibility = ["//visibility:public"],
)

proto_library(
name = "stats_proto",
srcs = ["stats.proto"],
visibility = ["//visibility:public"],
)
7 changes: 7 additions & 0 deletions ark_nova_stats/bga_log_parser/proto/map.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

package ark_nova_stats.bga_log_parser.proto;

message Map {

}
Loading

0 comments on commit a6454a4

Please sign in to comment.