Skip to content

Commit

Permalink
Merge pull request #29 from EboMike/cleanup
Browse files Browse the repository at this point in the history
Moved some code from the scorecard file to the helpers.
  • Loading branch information
spookybear0 authored Mar 26, 2024
2 parents 55caaa2 + a95e2e4 commit 7646f3f
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 48 deletions.
52 changes: 8 additions & 44 deletions handlers/game/scorecard.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from numpy import arange
from sanic import Request

from helpers.gamehelper import SM5_STATE_LABEL_MAP, SM5_STATE_COLORS, get_players_from_team
from shared import app
from typing import List
from utils import render_template
Expand All @@ -8,15 +10,9 @@
from db.game import EntityEnds, EntityStarts
from db.laserball import LaserballGame, LaserballStats
from helpers.statshelper import sentry_trace, _millis_to_time, count_zaps, count_missiles, count_blocks, \
get_player_state_distribution
get_player_state_distribution, get_sm5_score_components, get_sm5_kd_ratio
from sanic import exceptions

def get_players_from_team(all_players: List[dict], team_index: int) -> List[dict]:
"""Returns subset of the list of players - only those in the given team."""
return [
player for player in all_players if player["team"] == team_index
]


def _chart_values(values: list[int]) -> str:
"""Creates a string to be used in JavaScript for a list of integers."""
Expand Down Expand Up @@ -69,21 +65,7 @@ async def scorecard(request: Request, type: str, id: int, entity_end_id: int) ->
"Medic hits": stats.medic_hits,
}

bases_destroyed = await (game.events.filter(type=EventType.DESTROY_BASE).
filter(arguments__filter={"0": entity_start.entity_id}).count())

# Parts that make up the final score.
# Scores taken from https://www.iplaylaserforce.com/games/space-marines-sm5/
score_components = {
"Missiles": stats.missiled_opponent * 500,
"Zaps": stats.shot_opponent * 100,
"Bases": bases_destroyed * 1001,
"Nukes": stats.nukes_detonated * 500,
"Zap own team": stats.shot_team * -100,
"Missiled own team": stats.missiled_team * -500,
"Got zapped": stats.times_zapped * -20,
"Got missiled": stats.times_missiled * -100,
}
score_components = await get_sm5_score_components(game, stats, entity_start)

score_composition = [
{
Expand All @@ -96,29 +78,12 @@ async def scorecard(request: Request, type: str, id: int, entity_end_id: int) ->

score_composition.sort(key=lambda x: x["score"], reverse=True)

state_label_map = {
PlayerStateDetailType.ACTIVE: "Active",
PlayerStateDetailType.DOWN_ZAPPED: "Down",
PlayerStateDetailType.DOWN_MISSILED: "Down",
PlayerStateDetailType.DOWN_NUKED: "Down",
PlayerStateDetailType.DOWN_FOR_OTHER: "Down",
PlayerStateDetailType.DOWN_FOR_RESUP: "Down (Resup)",
PlayerStateDetailType.RESETTABLE: "Resettable",
}

state_colors = {
"Active": "#11dd11",
"Down": "#993202",
"Down (Resup)": "#8702ab",
"Resettable": "#cbd103",
}

state_distribution = await get_player_state_distribution(entity_start, entity_end, game.player_states, game.events,
state_label_map)
SM5_STATE_LABEL_MAP)

state_distribution_labels = list(state_distribution.keys())
state_distribution_values = list(state_distribution.values())
state_distribution_colors = [state_colors[state] for state in state_distribution.keys()]
state_distribution_colors = [SM5_STATE_COLORS[state] for state in state_distribution.keys()]

entity_starts: List[EntityStarts] = game.entity_starts
player_entities = [
Expand Down Expand Up @@ -150,15 +115,14 @@ async def scorecard(request: Request, type: str, id: int, entity_end_id: int) ->
"score": player_entity_ends[player.id].score,
"lives_left": player_sm5_stats[player.id].lives_left if player.id in player_sm5_stats else "",
"time_in_game_values": [player_entity_ends[player.id].time, game_duration - player_entity_ends[player.id].time],
"kd_ratio": ("%.2f" % (player_sm5_stats[player.id].shot_opponent / player_sm5_stats[player.id].times_zapped
if player_sm5_stats[player.id].times_zapped > 0 else 1)) if player.id in player_sm5_stats else "",
"kd_ratio": ("%.2f" % get_sm5_kd_ratio(player_sm5_stats[player.id])) if player.id in player_sm5_stats else "",
"mvp_points": "%.2f" % await player_sm5_stats[player.id].mvp_points(),
"you_zapped": await count_zaps(game, entity_start.entity_id, player.entity_id),
"zapped_you": await count_zaps(game, player.entity_id, entity_start.entity_id),
"you_missiled": await count_missiles(game, entity_start.entity_id, player.entity_id),
"missiled_you": await count_missiles(game, player.entity_id, entity_start.entity_id),
"state_distribution_values": list((await get_player_state_distribution(player, player_entity_ends[player.id],
game.player_states, game.events, state_label_map)).values())
game.player_states, game.events, SM5_STATE_LABEL_MAP)).values())
} for player in player_entities
])
all_players.sort(key=lambda x: x["score"], reverse=True)
Expand Down
41 changes: 41 additions & 0 deletions helpers/gamehelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Various constants and helpers when displaying stats about individual games.
This is different from statshelper in that it does not calculate stats, it
only deals with getting and extracting and visualizing data.
"""
from typing import List

from db.types import PlayerStateDetailType

"""Map of every possible player state and the display name for it in SM5 games.
Multiple player states map to the same name since they're intended to be lumped
together - in SM5, we don't care if a player is down because of a nuke or a
missile.
"""
SM5_STATE_LABEL_MAP = {
PlayerStateDetailType.ACTIVE: "Active",
PlayerStateDetailType.DOWN_ZAPPED: "Down",
PlayerStateDetailType.DOWN_MISSILED: "Down",
PlayerStateDetailType.DOWN_NUKED: "Down",
PlayerStateDetailType.DOWN_FOR_OTHER: "Down",
PlayerStateDetailType.DOWN_FOR_RESUP: "Down (Resup)",
PlayerStateDetailType.RESETTABLE: "Resettable",
}

"""Map of every SM5 player state and the color to use when visualizing it.
All keys in this dict map to values in SM5_STATE_LABEL_MAP."""
SM5_STATE_COLORS = {
"Active": "#11dd11",
"Down": "#993202",
"Down (Resup)": "#8702ab",
"Resettable": "#cbd103",
}


def get_players_from_team(all_players: List[dict], team_index: int) -> List[dict]:
"""Returns subset of the list of players - only those in the given team."""
return [
player for player in all_players if player["team"] == team_index
]
29 changes: 29 additions & 0 deletions helpers/statshelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ def _millis_to_time(milliseconds: Optional[int]) -> str:

return "%02d:%02d" % (milliseconds / 60000, milliseconds % 60000 / 1000)


async def get_sm5_score_components(game: SM5Game, stats: SM5Stats, entity_start: EntityStarts) -> dict[str, int]:
"""Returns a dict with individual components that make up a player's total score.
Each key is a component ("Missiles", "Nukes", etc), and the value is the amount of
points - positive or negative - the player got for all these."""
bases_destroyed = await (game.events.filter(type=EventType.DESTROY_BASE).
filter(arguments__filter={"0": entity_start.entity_id}).count())

# Scores taken from https://www.iplaylaserforce.com/games/space-marines-sm5/
return {
"Missiles": stats.missiled_opponent * 500,
"Zaps": stats.shot_opponent * 100,
"Bases": bases_destroyed * 1001,
"Nukes": stats.nukes_detonated * 500,
"Zap own team": stats.shot_team * -100,
"Missiled own team": stats.missiled_team * -500,
"Got zapped": stats.times_zapped * -20,
"Got missiled": stats.times_missiled * -100,
}


def get_sm5_kd_ratio(stats: SM5Stats) -> float:
"""Returns the K/D for a player.
This is the number of zaps (not downs) over the number of times the player got zapped.
1 if the player was never zapped."""
return stats.shot_opponent / stats.times_zapped if stats.times_zapped > 0 else 1.0

"""
Average score at time
Expand Down
9 changes: 8 additions & 1 deletion tests/helpers/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ async def create_zap_event(time_millis: int, zapping_entity_id: str, zapped_enti
["4", str(time_millis), EventType.DOWNED_OPPONENT, zapping_entity_id, " zaps ", zapped_entity_id])


async def create_destroy_base_event(time_millis: int, destroying_entity_id: str, base_entity_str: str) -> Events:
return await create_event_from_data(
["4", str(time_millis), EventType.DESTROY_BASE, destroying_entity_id, " destroys ", base_entity_str])


async def create_mission_end_event(time_millis) -> Events:
return await create_event_from_data(["4", str(time_millis), EventType.MISSION_END, "* Mission End *"])

Expand All @@ -123,7 +128,7 @@ async def add_entity(
member_id: str = "4-43-000",
score: int = 0,
sm5_game: Optional[SM5Game] = None
):
) -> (EntityStarts, EntityEnds):
entity_start = await create_entity_start(
entity_id=entity_id,
team=team,
Expand All @@ -146,6 +151,8 @@ async def add_entity(
await sm5_game.entity_starts.add(entity_start)
await sm5_game.entity_ends.add(entity_end)

return entity_start, entity_end


async def create_entity_start(
entity_id: str,
Expand Down
37 changes: 37 additions & 0 deletions tests/helpers/gamehelper_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import unittest

from db.sm5 import SM5Game, SM5Stats
from helpers.gamehelper import get_players_from_team
from helpers.statshelper import count_zaps, get_sm5_kd_ratio, get_sm5_score_components
from tests.helpers.environment import setup_test_database, ENTITY_ID_1, ENTITY_ID_2, get_sm5_game_id, \
teardown_test_database, create_destroy_base_event, add_entity, get_red_team


class TestGameHelper(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
await setup_test_database()

async def asyncTearDown(self):
await teardown_test_database()

async def test_get_players_from_team(self):
player1 = {
"name": "Indy",
"team": 0,
}
player2 = {
"name": "Barbie",
"team": 1,
}
player3 = {
"name": "Sonic",
"team": 1,
}

players_in_team = get_players_from_team([player1, player2, player3], team_index=1)

self.assertCountEqual([player2, player3], players_in_team)


if __name__ == '__main__':
unittest.main()
54 changes: 51 additions & 3 deletions tests/helpers/statshelper_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import unittest

from db.sm5 import SM5Game
from helpers.statshelper import count_zaps
from tests.helpers.environment import setup_test_database, ENTITY_ID_1, ENTITY_ID_2, get_sm5_game_id, teardown_test_database
from db.sm5 import SM5Game, SM5Stats
from helpers.statshelper import count_zaps, get_sm5_kd_ratio, get_sm5_score_components
from tests.helpers.environment import setup_test_database, ENTITY_ID_1, ENTITY_ID_2, get_sm5_game_id, \
teardown_test_database, create_destroy_base_event, add_entity, get_red_team


class TestStatsHelper(unittest.IsolatedAsyncioTestCase):
Expand All @@ -17,6 +18,53 @@ async def test_count_zaps(self):
zaps = await count_zaps(game, ENTITY_ID_1, ENTITY_ID_2)
self.assertEqual(3, zaps)

async def test_get_sm5_score_components(self):
game = await SM5Game.filter(id=get_sm5_game_id()).first()
await game.events.add(await create_destroy_base_event(time_millis=10000,
destroying_entity_id=ENTITY_ID_1,
base_entity_str="@red_base"))
await game.events.add(await create_destroy_base_event(time_millis=20000,
destroying_entity_id=ENTITY_ID_1,
base_entity_str="@reactor"))
entity_start, entity_end = await add_entity(entity_id=ENTITY_ID_1, team=get_red_team())

stats = SM5Stats(
missiled_opponent=3,
shot_opponent=5,
nukes_detonated=7,
shot_team=11,
missiled_team=13,
times_zapped=17,
times_missiled=23,
)

self.assertDictEqual({
"Missiles": 1500,
"Zaps": 500,
"Bases": 2002,
"Nukes": 3500,
"Zap own team": -1100,
"Missiled own team": -6500,
"Got zapped": -340,
"Got missiled": -2300,
}, await get_sm5_score_components(game, stats, entity_start))

async def test_get_kd_ratio(self):
stats = SM5Stats(
shot_opponent=10,
times_zapped=2,
)

self.assertEqual(5.0, get_sm5_kd_ratio(stats))

async def test_get_kd_ratio_never_zapped(self):
stats = SM5Stats(
shot_opponent=10,
times_zapped=0,
)

self.assertEqual(1.0, get_sm5_kd_ratio(stats))


if __name__ == '__main__':
unittest.main()

0 comments on commit 7646f3f

Please sign in to comment.