Compare commits
2 commits
03f0e048af
...
72f80563f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f80563f1 | ||
|
|
b1bde76319 |
16 changed files with 328 additions and 246 deletions
|
|
@ -308,4 +308,8 @@ class ContextType(StrEnum):
|
||||||
CAMPAIGN = auto()
|
CAMPAIGN = auto()
|
||||||
CHOICE = auto()
|
CHOICE = auto()
|
||||||
BATTLE = auto()
|
BATTLE = auto()
|
||||||
OBJECTIVE = auto()
|
|
||||||
|
|
||||||
|
class ScoreKind(Enum):
|
||||||
|
VP = auto()
|
||||||
|
NP = auto()
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
from typing import List, Dict, Tuple, TYPE_CHECKING
|
from typing import List, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QMessageBox, QDialog
|
from PyQt6.QtWidgets import QMessageBox, QDialog
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
|
||||||
from warchron.constants import (
|
from warchron.constants import RefreshScope, ContextType, ItemType
|
||||||
RefreshScope,
|
|
||||||
ContextType,
|
|
||||||
ItemType,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
from warchron.controller.app_controller import AppController
|
||||||
|
|
@ -68,7 +64,7 @@ class CampaignController:
|
||||||
objective_icon_maps[obj.id] = RankingIcon.compute_icons(
|
objective_icon_maps[obj.id] = RankingIcon.compute_icons(
|
||||||
war,
|
war,
|
||||||
ContextType.CAMPAIGN,
|
ContextType.CAMPAIGN,
|
||||||
f"{camp.id}:{obj.id}",
|
camp.id,
|
||||||
scores,
|
scores,
|
||||||
objective_id=obj.id,
|
objective_id=obj.id,
|
||||||
)
|
)
|
||||||
|
|
@ -170,12 +166,10 @@ class CampaignController:
|
||||||
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
active = TieResolver.get_active_participants(
|
active = TieResolver.get_active_participants(war, ctx, ctx.participants)
|
||||||
war, ctx.context_type, ctx.context_id, ctx.participants
|
|
||||||
)
|
|
||||||
players = [
|
players = [
|
||||||
ParticipantOption(id=pid, name=self.app.model.get_participant_name(pid))
|
ParticipantOption(id=pid, name=self.app.model.get_participant_name(pid))
|
||||||
for pid in active
|
for pid in active
|
||||||
|
|
@ -189,9 +183,8 @@ class CampaignController:
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=None,
|
context_name=None,
|
||||||
)
|
)
|
||||||
if ctx.context_type == ContextType.OBJECTIVE:
|
if ctx.objective_id:
|
||||||
campaign_id, objective_id = ctx.context_id.split(":")
|
objective = war.objectives[ctx.objective_id]
|
||||||
objective = war.objectives[objective_id]
|
|
||||||
dialog = TieDialog(
|
dialog = TieDialog(
|
||||||
parent=self.app.view,
|
parent=self.app.view,
|
||||||
players=players,
|
players=players,
|
||||||
|
|
@ -201,13 +194,10 @@ class CampaignController:
|
||||||
context_name=objective.name,
|
context_name=objective.name,
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
TieResolver.cancel_tie_break(
|
# FIXME lost tokens used for narrative tie-break (ContextType.OBJECTIVE)
|
||||||
war, ContextType.CAMPAIGN, ctx.context_id, ctx.score_value
|
TieResolver.cancel_tie_break(war, ctx)
|
||||||
)
|
|
||||||
raise ForbiddenOperation("Tie resolution cancelled")
|
raise ForbiddenOperation("Tie resolution cancelled")
|
||||||
bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = (
|
bids_map[ctx.key()] = dialog.get_bids()
|
||||||
dialog.get_bids()
|
|
||||||
)
|
|
||||||
return bids_map
|
return bids_map
|
||||||
|
|
||||||
# Campaign participant methods
|
# Campaign participant methods
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ class RoundClosureWorkflow(ClosureWorkflow):
|
||||||
while ties:
|
while ties:
|
||||||
bids_map = self.app.rounds.resolve_ties(war, ties)
|
bids_map = self.app.rounds.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(war, tie, bids)
|
||||||
ties = TieResolver.find_battle_ties(war, round.id)
|
ties = TieResolver.find_battle_ties(war, round.id)
|
||||||
for battle in round.battles.values():
|
for battle in round.battles.values():
|
||||||
|
|
@ -40,8 +40,8 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
while ties:
|
while ties:
|
||||||
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(war, tie, bids)
|
||||||
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
||||||
for objective_id in war.objectives:
|
for objective_id in war.objectives:
|
||||||
|
|
@ -53,8 +53,8 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
while ties:
|
while ties:
|
||||||
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(war, tie, bids)
|
||||||
ties = TieResolver.find_campaign_objective_ties(
|
ties = TieResolver.find_campaign_objective_ties(
|
||||||
war,
|
war,
|
||||||
|
|
@ -72,8 +72,8 @@ class WarClosureWorkflow(ClosureWorkflow):
|
||||||
while ties:
|
while ties:
|
||||||
bids_map = self.app.wars.resolve_ties(war, ties)
|
bids_map = self.app.wars.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(war, tie, bids)
|
||||||
ties = TieResolver.find_war_ties(war)
|
ties = TieResolver.find_war_ties(war)
|
||||||
for objective_id in war.objectives:
|
for objective_id in war.objectives:
|
||||||
|
|
@ -84,8 +84,8 @@ class WarClosureWorkflow(ClosureWorkflow):
|
||||||
while ties:
|
while ties:
|
||||||
bids_map = self.app.wars.resolve_ties(war, ties)
|
bids_map = self.app.wars.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(war, tie, bids)
|
||||||
ties = TieResolver.find_war_objective_ties(
|
ties = TieResolver.find_war_objective_ties(
|
||||||
war,
|
war,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from warchron.constants import (
|
||||||
IconName,
|
IconName,
|
||||||
VP_RANK_TO_ICON,
|
VP_RANK_TO_ICON,
|
||||||
NP_RANK_TO_ICON,
|
NP_RANK_TO_ICON,
|
||||||
|
ScoreKind,
|
||||||
)
|
)
|
||||||
from warchron.model.war import War
|
from warchron.model.war import War
|
||||||
from warchron.model.score_service import ParticipantScore
|
from warchron.model.score_service import ParticipantScore
|
||||||
|
|
@ -31,14 +32,22 @@ class RankingIcon:
|
||||||
return score.victory_points
|
return score.victory_points
|
||||||
|
|
||||||
icon_ranking = VP_RANK_TO_ICON
|
icon_ranking = VP_RANK_TO_ICON
|
||||||
|
score_kind = ScoreKind.VP
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def value_getter(score: ParticipantScore) -> int:
|
def value_getter(score: ParticipantScore) -> int:
|
||||||
return score.narrative_points.get(objective_id, 0)
|
return score.narrative_points.get(objective_id, 0)
|
||||||
|
|
||||||
icon_ranking = NP_RANK_TO_ICON
|
icon_ranking = NP_RANK_TO_ICON
|
||||||
|
score_kind = ScoreKind.NP
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
ranking = ResultChecker.get_effective_ranking(
|
||||||
war, context_type, context_id, scores, value_getter=value_getter
|
war,
|
||||||
|
context_type,
|
||||||
|
context_id,
|
||||||
|
score_kind,
|
||||||
|
scores,
|
||||||
|
value_getter,
|
||||||
|
objective_id,
|
||||||
)
|
)
|
||||||
icon_map: Dict[str, QIcon] = {}
|
icon_map: Dict[str, QIcon] = {}
|
||||||
for rank, group, token_map in ranking:
|
for rank, group, token_map in ranking:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import List, Dict, Tuple, TYPE_CHECKING
|
from typing import List, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QDialog
|
from PyQt6.QtWidgets import QDialog
|
||||||
from PyQt6.QtWidgets import QMessageBox
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
|
@ -105,9 +105,8 @@ class RoundController:
|
||||||
if battle.is_draw():
|
if battle.is_draw():
|
||||||
p1_icon = Icons.get(IconName.DRAW)
|
p1_icon = Icons.get(IconName.DRAW)
|
||||||
p2_icon = Icons.get(IconName.DRAW)
|
p2_icon = Icons.get(IconName.DRAW)
|
||||||
if TieResolver.was_tie_broken_by_tokens(
|
context = TieContext(ContextType.BATTLE, battle.sector_id)
|
||||||
war, ContextType.BATTLE, battle.sector_id
|
if TieResolver.was_tie_broken_by_tokens(war, context):
|
||||||
):
|
|
||||||
effective_winner = ResultChecker.get_effective_winner_id(
|
effective_winner = ResultChecker.get_effective_winner_id(
|
||||||
war, ContextType.BATTLE, battle.sector_id, None
|
war, ContextType.BATTLE, battle.sector_id, None
|
||||||
)
|
)
|
||||||
|
|
@ -179,7 +178,7 @@ class RoundController:
|
||||||
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
players = [
|
players = [
|
||||||
|
|
@ -198,13 +197,9 @@ class RoundController:
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
TieResolver.cancel_tie_break(
|
TieResolver.cancel_tie_break(war, ctx)
|
||||||
war, ContextType.BATTLE, ctx.context_id, ctx.score_value
|
|
||||||
)
|
|
||||||
raise ForbiddenOperation("Tie resolution cancelled")
|
raise ForbiddenOperation("Tie resolution cancelled")
|
||||||
bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = (
|
bids_map[ctx.key()] = dialog.get_bids()
|
||||||
dialog.get_bids()
|
|
||||||
)
|
|
||||||
return bids_map
|
return bids_map
|
||||||
|
|
||||||
# Choice methods
|
# Choice methods
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import List, Tuple, TYPE_CHECKING, Dict
|
from typing import List, TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QMessageBox, QDialog
|
from PyQt6.QtWidgets import QMessageBox, QDialog
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
|
@ -64,7 +64,7 @@ class WarController:
|
||||||
objective_icon_maps[obj.id] = RankingIcon.compute_icons(
|
objective_icon_maps[obj.id] = RankingIcon.compute_icons(
|
||||||
war,
|
war,
|
||||||
ContextType.WAR,
|
ContextType.WAR,
|
||||||
f"{war.id}:{obj.id}",
|
war.id,
|
||||||
scores,
|
scores,
|
||||||
objective_id=obj.id,
|
objective_id=obj.id,
|
||||||
)
|
)
|
||||||
|
|
@ -151,16 +151,14 @@ class WarController:
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# FIXME tie dialog with all participant even without tie
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
active = TieResolver.get_active_participants(
|
active = TieResolver.get_active_participants(
|
||||||
war,
|
war,
|
||||||
ctx.context_type,
|
ctx,
|
||||||
ctx.context_id,
|
|
||||||
ctx.participants,
|
ctx.participants,
|
||||||
)
|
)
|
||||||
players = [
|
players = [
|
||||||
|
|
@ -176,25 +174,21 @@ class WarController:
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=None,
|
context_name=None,
|
||||||
)
|
)
|
||||||
if ctx.context_type == ContextType.OBJECTIVE:
|
if ctx.objective_id:
|
||||||
_, objective_id = ctx.context_id.split(":")
|
objective = war.objectives[ctx.objective_id]
|
||||||
objective = war.objectives[objective_id]
|
|
||||||
dialog = TieDialog(
|
dialog = TieDialog(
|
||||||
parent=self.app.view,
|
parent=self.app.view,
|
||||||
players=players,
|
players=players,
|
||||||
counters=counters,
|
counters=counters,
|
||||||
context_type=ctx.context_type,
|
context_type=ctx.context_type,
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=objective.name,
|
context_name=f"Objective tie: {objective.name}",
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
TieResolver.cancel_tie_break(
|
# FIXME lost tokens used for narrative tie-break (ContextType.OBJECTIVE)
|
||||||
war, ContextType.WAR, ctx.context_id, ctx.score_value
|
TieResolver.cancel_tie_break(war, ctx)
|
||||||
)
|
|
||||||
raise ForbiddenOperation("Tie resolution cancelled")
|
raise ForbiddenOperation("Tie resolution cancelled")
|
||||||
bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = (
|
bids_map[ctx.key()] = dialog.get_bids()
|
||||||
dialog.get_bids()
|
|
||||||
)
|
|
||||||
return bids_map
|
return bids_map
|
||||||
|
|
||||||
def set_major_value(self, value: int) -> None:
|
def set_major_value(self, value: int) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from warchron.model.json_helper import JsonHelper
|
||||||
|
|
||||||
|
|
||||||
class Battle:
|
class Battle:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -71,11 +73,13 @@ class Battle:
|
||||||
def fromDict(data: Dict[str, Any]) -> Battle:
|
def fromDict(data: Dict[str, Any]) -> Battle:
|
||||||
battle = Battle(
|
battle = Battle(
|
||||||
data["sector_id"],
|
data["sector_id"],
|
||||||
data.get("player_1_id") or None,
|
JsonHelper.none_if_empty(data.get("player_1_id")),
|
||||||
data.get("player_2_id") or None,
|
JsonHelper.none_if_empty(data.get("player_2_id")),
|
||||||
)
|
)
|
||||||
battle.winner_id = data.get("winner_id") or None
|
battle.winner_id = JsonHelper.none_if_empty(data.get("winner_id"))
|
||||||
battle.score = data.get("score") or None
|
battle.score = JsonHelper.none_if_empty(data.get("score"))
|
||||||
battle.victory_condition = data.get("victory_condition") or None
|
battle.victory_condition = JsonHelper.none_if_empty(
|
||||||
battle.comment = data.get("comment") or None
|
data.get("victory_condition")
|
||||||
|
)
|
||||||
|
battle.comment = JsonHelper.none_if_empty(data.get("comment"))
|
||||||
return battle
|
return battle
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from warchron.model.json_helper import JsonHelper
|
||||||
|
|
||||||
|
|
||||||
class Choice:
|
class Choice:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -42,8 +44,8 @@ class Choice:
|
||||||
def fromDict(data: Dict[str, Any]) -> Choice:
|
def fromDict(data: Dict[str, Any]) -> Choice:
|
||||||
choice = Choice(
|
choice = Choice(
|
||||||
data["participant_id"],
|
data["participant_id"],
|
||||||
data.get("priority_sector_id") or None,
|
JsonHelper.none_if_empty(data.get("priority_sector_id")),
|
||||||
data.get("secondary_sector_id") or None,
|
JsonHelper.none_if_empty(data.get("secondary_sector_id")),
|
||||||
)
|
)
|
||||||
choice.comment = data.get("comment") or None
|
choice.comment = JsonHelper.none_if_empty(data.get("comment"))
|
||||||
return choice
|
return choice
|
||||||
|
|
|
||||||
11
src/warchron/model/json_helper.py
Normal file
11
src/warchron/model/json_helper.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class JsonHelper:
|
||||||
|
@staticmethod
|
||||||
|
def none_if_empty(value: T | None) -> T | None:
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from warchron.model.json_helper import JsonHelper
|
||||||
|
|
||||||
|
|
||||||
class Objective:
|
class Objective:
|
||||||
def __init__(self, name: str, description: str | None):
|
def __init__(self, name: str, description: str | None):
|
||||||
|
|
@ -27,6 +29,6 @@ class Objective:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fromDict(data: Dict[str, Any]) -> Objective:
|
def fromDict(data: Dict[str, Any]) -> Objective:
|
||||||
obj = Objective(data["name"], data["description"] or None)
|
obj = Objective(data["name"], JsonHelper.none_if_empty(data["description"]))
|
||||||
obj.set_id(data["id"])
|
obj.set_id(data["id"])
|
||||||
return obj
|
return obj
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
||||||
from typing import List, Tuple, Dict, TYPE_CHECKING, Callable
|
from typing import List, Tuple, Dict, TYPE_CHECKING, Callable
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from warchron.constants import ContextType
|
from warchron.constants import ContextType, ScoreKind
|
||||||
from warchron.model.war import War
|
from warchron.model.war import War
|
||||||
from warchron.model.war_event import TieResolved
|
from warchron.model.war_event import TieResolved
|
||||||
from warchron.model.tie_manager import TieResolver
|
from warchron.model.tie_manager import TieResolver, TieContext
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.model.score_service import ParticipantScore
|
from warchron.model.score_service import ParticipantScore
|
||||||
|
|
@ -35,19 +35,37 @@ class ResultChecker:
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context_type: ContextType,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
|
score_kind: ScoreKind | None,
|
||||||
scores: Dict[str, ParticipantScore],
|
scores: Dict[str, ParticipantScore],
|
||||||
value_getter: Callable[[ParticipantScore], int],
|
value_getter: Callable[[ParticipantScore], int],
|
||||||
|
objective_id: str | None = None,
|
||||||
) -> List[Tuple[int, List[str], Dict[str, int]]]:
|
) -> List[Tuple[int, List[str], Dict[str, int]]]:
|
||||||
buckets: Dict[int, List[str]] = defaultdict(list)
|
buckets: Dict[int, List[str]] = defaultdict(list)
|
||||||
for pid, score in scores.items():
|
for pid, score in scores.items():
|
||||||
buckets[value_getter(score)].append(pid)
|
buckets[value_getter(score)].append(pid)
|
||||||
sorted_vps = sorted(buckets.keys(), reverse=True)
|
sorted_values = sorted(buckets.keys(), reverse=True)
|
||||||
ranking: List[Tuple[int, List[str], Dict[str, int]]] = []
|
ranking: List[Tuple[int, List[str], Dict[str, int]]] = []
|
||||||
current_rank = 1
|
current_rank = 1
|
||||||
for value in sorted_vps:
|
assert score_kind is not None
|
||||||
|
for value in sorted_values:
|
||||||
participants = buckets[value]
|
participants = buckets[value]
|
||||||
|
context: TieContext = TieContext(
|
||||||
|
context_type,
|
||||||
|
context_id,
|
||||||
|
participants,
|
||||||
|
value,
|
||||||
|
score_kind,
|
||||||
|
objective_id,
|
||||||
|
)
|
||||||
if context_type == ContextType.WAR and len(participants) > 1:
|
if context_type == ContextType.WAR and len(participants) > 1:
|
||||||
subgroups = ResultChecker._secondary_sorting_war(war, participants)
|
subcontexts = ResultChecker._build_war_subcontexts(
|
||||||
|
war,
|
||||||
|
score_kind,
|
||||||
|
objective_id,
|
||||||
|
)
|
||||||
|
subgroups = ResultChecker._secondary_sorting_war(
|
||||||
|
war, participants, value_getter, subcontexts
|
||||||
|
)
|
||||||
for subgroup in subgroups:
|
for subgroup in subgroups:
|
||||||
# no tie if campaigns' rank is enough to sort
|
# no tie if campaigns' rank is enough to sort
|
||||||
if len(subgroup) == 1:
|
if len(subgroup) == 1:
|
||||||
|
|
@ -57,47 +75,29 @@ class ResultChecker:
|
||||||
current_rank += 1
|
current_rank += 1
|
||||||
continue
|
continue
|
||||||
# normal tie-break if tie persists
|
# normal tie-break if tie persists
|
||||||
if not TieResolver.is_tie_resolved(
|
if not TieResolver.is_tie_resolved(war, context):
|
||||||
war, context_type, context_id, value
|
|
||||||
):
|
|
||||||
ranking.append(
|
ranking.append(
|
||||||
(current_rank, subgroup, {pid: 0 for pid in subgroup})
|
(current_rank, subgroup, {pid: 0 for pid in subgroup})
|
||||||
)
|
)
|
||||||
current_rank += 1
|
current_rank += 1
|
||||||
continue
|
continue
|
||||||
groups = TieResolver.rank_by_tokens(
|
groups = TieResolver.rank_by_tokens(war, context, subgroup)
|
||||||
war,
|
tokens_spent = TieResolver.tokens_spent_map(war, context, subgroup)
|
||||||
context_type,
|
|
||||||
context_id,
|
|
||||||
subgroup,
|
|
||||||
)
|
|
||||||
tokens_spent = TieResolver.tokens_spent_map(
|
|
||||||
war, context_type, context_id, subgroup
|
|
||||||
)
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group_tokens = {pid: tokens_spent[pid] for pid in group}
|
group_tokens = {pid: tokens_spent[pid] for pid in group}
|
||||||
ranking.append((current_rank, group, group_tokens))
|
ranking.append((current_rank, group, group_tokens))
|
||||||
current_rank += 1
|
current_rank += 1
|
||||||
continue
|
continue
|
||||||
# no tie
|
# no tie
|
||||||
if len(participants) == 1 or not TieResolver.is_tie_resolved(
|
if len(participants) == 1 or not TieResolver.is_tie_resolved(war, context):
|
||||||
war, context_type, context_id, value
|
|
||||||
):
|
|
||||||
ranking.append(
|
ranking.append(
|
||||||
(current_rank, participants, {pid: 0 for pid in participants})
|
(current_rank, participants, {pid: 0 for pid in participants})
|
||||||
)
|
)
|
||||||
current_rank += 1
|
current_rank += 1
|
||||||
continue
|
continue
|
||||||
# apply token ranking
|
# apply token ranking
|
||||||
groups = TieResolver.rank_by_tokens(
|
groups = TieResolver.rank_by_tokens(war, context, participants)
|
||||||
war,
|
tokens_spent = TieResolver.tokens_spent_map(war, context, participants)
|
||||||
context_type,
|
|
||||||
context_id,
|
|
||||||
participants,
|
|
||||||
)
|
|
||||||
tokens_spent = TieResolver.tokens_spent_map(
|
|
||||||
war, context_type, context_id, participants
|
|
||||||
)
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group_tokens = {pid: tokens_spent[pid] for pid in group}
|
group_tokens = {pid: tokens_spent[pid] for pid in group}
|
||||||
ranking.append((current_rank, group, group_tokens))
|
ranking.append((current_rank, group, group_tokens))
|
||||||
|
|
@ -108,27 +108,35 @@ class ResultChecker:
|
||||||
def _secondary_sorting_war(
|
def _secondary_sorting_war(
|
||||||
war: War,
|
war: War,
|
||||||
participants: List[str],
|
participants: List[str],
|
||||||
|
value_getter: Callable[[ParticipantScore], int],
|
||||||
|
subcontexts: List[TieContext],
|
||||||
) -> List[List[str]]:
|
) -> List[List[str]]:
|
||||||
from warchron.model.score_service import ScoreService
|
from warchron.model.score_service import ScoreService
|
||||||
|
|
||||||
rank_map: Dict[str, Tuple[int, ...]] = {}
|
rank_map: Dict[str, Tuple[int, ...]] = {}
|
||||||
for pid in participants:
|
for pid in participants:
|
||||||
ranks: List[int] = []
|
ranks: List[int] = []
|
||||||
for campaign in war.campaigns:
|
for sub in subcontexts:
|
||||||
scores = ScoreService.compute_scores(
|
scores = ScoreService.compute_scores(
|
||||||
war, ContextType.CAMPAIGN, campaign.id
|
war, sub.context_type, sub.context_id
|
||||||
)
|
)
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
ranking = ResultChecker.get_effective_ranking(
|
||||||
war,
|
war,
|
||||||
ContextType.CAMPAIGN,
|
sub.context_type,
|
||||||
campaign.id,
|
sub.context_id,
|
||||||
|
sub.score_kind,
|
||||||
scores,
|
scores,
|
||||||
lambda s: s.victory_points,
|
value_getter,
|
||||||
|
sub.objective_id,
|
||||||
)
|
)
|
||||||
|
found = False
|
||||||
for rank, group, _ in ranking:
|
for rank, group, _ in ranking:
|
||||||
if pid in group:
|
if pid in group:
|
||||||
ranks.append(rank)
|
ranks.append(rank)
|
||||||
|
found = True
|
||||||
break
|
break
|
||||||
|
if not found:
|
||||||
|
ranks.append(len(scores) + 1)
|
||||||
rank_map[pid] = tuple(ranks)
|
rank_map[pid] = tuple(ranks)
|
||||||
sorted_items = sorted(rank_map.items(), key=lambda x: x[1])
|
sorted_items = sorted(rank_map.items(), key=lambda x: x[1])
|
||||||
groups: List[List[str]] = []
|
groups: List[List[str]] = []
|
||||||
|
|
@ -139,3 +147,23 @@ class ResultChecker:
|
||||||
current_tuple = rank_tuple
|
current_tuple = rank_tuple
|
||||||
groups[-1].append(pid)
|
groups[-1].append(pid)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_war_subcontexts(
|
||||||
|
war: War,
|
||||||
|
score_kind: ScoreKind,
|
||||||
|
objective_id: str | None,
|
||||||
|
) -> List[TieContext]:
|
||||||
|
subcontexts = []
|
||||||
|
for campaign in war.campaigns:
|
||||||
|
subcontexts.append(
|
||||||
|
TieContext(
|
||||||
|
ContextType.CAMPAIGN,
|
||||||
|
campaign.id,
|
||||||
|
[],
|
||||||
|
None,
|
||||||
|
score_kind,
|
||||||
|
objective_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return subcontexts
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from warchron.model.json_helper import JsonHelper
|
||||||
|
|
||||||
|
|
||||||
class Sector:
|
class Sector:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -64,11 +66,11 @@ class Sector:
|
||||||
sec = Sector(
|
sec = Sector(
|
||||||
data["name"],
|
data["name"],
|
||||||
data["round_id"],
|
data["round_id"],
|
||||||
data.get("major_objective_id") or None,
|
JsonHelper.none_if_empty(data.get("major_objective_id")),
|
||||||
data.get("minor_objective_id") or None,
|
JsonHelper.none_if_empty(data.get("minor_objective_id")),
|
||||||
data.get("influence_objective_id") or None,
|
JsonHelper.none_if_empty(data.get("influence_objective_id")),
|
||||||
data.get("mission") or None,
|
JsonHelper.none_if_empty(data.get("mission")),
|
||||||
data.get("description") or None,
|
JsonHelper.none_if_empty(data.get("description")),
|
||||||
)
|
)
|
||||||
sec.set_id(data["id"])
|
sec.set_id(data["id"])
|
||||||
sec.mission = data.get("mission") or None
|
sec.mission = data.get("mission") or None
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
from typing import List, Dict, DefaultDict
|
from typing import List, Dict, DefaultDict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from warchron.constants import ContextType
|
from warchron.constants import ContextType, ScoreKind
|
||||||
from warchron.model.exception import ForbiddenOperation
|
from warchron.model.exception import ForbiddenOperation
|
||||||
from warchron.model.war import War
|
from warchron.model.war import War
|
||||||
from warchron.model.war_event import InfluenceSpent, TieResolved
|
from warchron.model.war_event import InfluenceSpent, TieResolved
|
||||||
|
|
@ -13,8 +13,13 @@ from warchron.model.score_service import ScoreService, ParticipantScore
|
||||||
class TieContext:
|
class TieContext:
|
||||||
context_type: ContextType
|
context_type: ContextType
|
||||||
context_id: str
|
context_id: str
|
||||||
participants: List[str] # war_participant_ids
|
participants: List[str] = field(default_factory=list) # war_participant_ids
|
||||||
score_value: int | None = None
|
score_value: int | None = None
|
||||||
|
score_kind: ScoreKind | None = None
|
||||||
|
objective_id: str | None = None
|
||||||
|
|
||||||
|
def key(self) -> tuple[str, str, int | None]:
|
||||||
|
return (self.context_type, self.context_id, self.score_value)
|
||||||
|
|
||||||
|
|
||||||
class TieResolver:
|
class TieResolver:
|
||||||
|
|
@ -27,9 +32,11 @@ class TieResolver:
|
||||||
for battle in round.battles.values():
|
for battle in round.battles.values():
|
||||||
if not battle.is_draw():
|
if not battle.is_draw():
|
||||||
continue
|
continue
|
||||||
if TieResolver.is_tie_resolved(
|
context: TieContext = TieContext(
|
||||||
war, ContextType.BATTLE, battle.sector_id, None
|
ContextType.BATTLE,
|
||||||
):
|
battle.sector_id,
|
||||||
|
)
|
||||||
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
if campaign is None:
|
if campaign is None:
|
||||||
raise RuntimeError("No campaign for this battle tie")
|
raise RuntimeError("No campaign for this battle tie")
|
||||||
|
|
@ -37,9 +44,7 @@ class TieResolver:
|
||||||
raise RuntimeError("Missing player(s) in this battle context.")
|
raise RuntimeError("Missing player(s) in this battle context.")
|
||||||
p1 = campaign.participants[battle.player_1_id].war_participant_id
|
p1 = campaign.participants[battle.player_1_id].war_participant_id
|
||||||
p2 = campaign.participants[battle.player_2_id].war_participant_id
|
p2 = campaign.participants[battle.player_2_id].war_participant_id
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(war, context, [p1, p2]):
|
||||||
war, ContextType.BATTLE, battle.sector_id, [p1, p2]
|
|
||||||
):
|
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(None, ContextType.BATTLE, battle.sector_id)
|
TieResolved(None, ContextType.BATTLE, battle.sector_id)
|
||||||
)
|
)
|
||||||
|
|
@ -50,6 +55,7 @@ class TieResolver:
|
||||||
context_id=battle.sector_id,
|
context_id=battle.sector_id,
|
||||||
participants=[p1, p2],
|
participants=[p1, p2],
|
||||||
score_value=None,
|
score_value=None,
|
||||||
|
score_kind=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
@ -64,15 +70,23 @@ class TieResolver:
|
||||||
for score_value, participants in buckets.items():
|
for score_value, participants in buckets.items():
|
||||||
if len(participants) <= 1:
|
if len(participants) <= 1:
|
||||||
continue
|
continue
|
||||||
if TieResolver.is_tie_resolved(
|
context: TieContext = TieContext(
|
||||||
war, ContextType.CAMPAIGN, campaign_id, score_value
|
ContextType.CAMPAIGN,
|
||||||
):
|
campaign_id,
|
||||||
|
[],
|
||||||
|
score_value,
|
||||||
|
ScoreKind.VP,
|
||||||
|
)
|
||||||
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(war, context, participants):
|
||||||
war, ContextType.CAMPAIGN, campaign_id, participants
|
|
||||||
):
|
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(None, ContextType.CAMPAIGN, campaign_id, score_value)
|
TieResolved(
|
||||||
|
None,
|
||||||
|
ContextType.CAMPAIGN,
|
||||||
|
campaign_id,
|
||||||
|
score_value,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
|
|
@ -81,6 +95,7 @@ class TieResolver:
|
||||||
context_id=campaign_id,
|
context_id=campaign_id,
|
||||||
participants=participants,
|
participants=participants,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
|
score_kind=ScoreKind.VP,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
@ -91,51 +106,49 @@ class TieResolver:
|
||||||
campaign_id: str,
|
campaign_id: str,
|
||||||
objective_id: str,
|
objective_id: str,
|
||||||
) -> List[TieContext]:
|
) -> List[TieContext]:
|
||||||
base_scores = ScoreService.compute_scores(
|
scores = ScoreService.compute_scores(
|
||||||
war,
|
war,
|
||||||
ContextType.CAMPAIGN,
|
ContextType.CAMPAIGN,
|
||||||
campaign_id,
|
campaign_id,
|
||||||
)
|
)
|
||||||
scores = TieResolver._build_objective_scores(
|
|
||||||
base_scores,
|
|
||||||
objective_id,
|
|
||||||
)
|
|
||||||
buckets: DefaultDict[int, List[str]] = defaultdict(list)
|
buckets: DefaultDict[int, List[str]] = defaultdict(list)
|
||||||
for pid, score in scores.items():
|
for pid, score in scores.items():
|
||||||
buckets[score.victory_points].append(pid)
|
np_value = score.narrative_points.get(objective_id, 0)
|
||||||
|
buckets[np_value].append(pid)
|
||||||
ties: List[TieContext] = []
|
ties: List[TieContext] = []
|
||||||
context_id = f"{campaign_id}:{objective_id}"
|
context_id = campaign_id
|
||||||
for score_value, participants in buckets.items():
|
for np_value, participants in buckets.items():
|
||||||
if len(participants) <= 1:
|
if len(participants) <= 1:
|
||||||
continue
|
continue
|
||||||
if TieResolver.is_tie_resolved(
|
context: TieContext = TieContext(
|
||||||
war,
|
ContextType.CAMPAIGN,
|
||||||
ContextType.OBJECTIVE,
|
campaign_id,
|
||||||
context_id,
|
[],
|
||||||
score_value,
|
np_value,
|
||||||
):
|
ScoreKind.NP,
|
||||||
|
objective_id,
|
||||||
|
)
|
||||||
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war,
|
war,
|
||||||
ContextType.OBJECTIVE,
|
context,
|
||||||
context_id,
|
|
||||||
participants,
|
participants,
|
||||||
):
|
):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
TieResolved(
|
||||||
None,
|
None, ContextType.CAMPAIGN, context_id, np_value, objective_id
|
||||||
ContextType.OBJECTIVE,
|
|
||||||
context_id,
|
|
||||||
score_value,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
TieContext(
|
TieContext(
|
||||||
context_type=ContextType.OBJECTIVE,
|
context_type=ContextType.CAMPAIGN,
|
||||||
context_id=context_id,
|
context_id=context_id,
|
||||||
participants=participants,
|
participants=participants,
|
||||||
score_value=score_value,
|
score_value=np_value,
|
||||||
|
score_kind=ScoreKind.NP,
|
||||||
|
objective_id=objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
@ -149,17 +162,25 @@ class TieResolver:
|
||||||
war,
|
war,
|
||||||
ContextType.WAR,
|
ContextType.WAR,
|
||||||
war.id,
|
war.id,
|
||||||
|
ScoreKind.VP,
|
||||||
scores,
|
scores,
|
||||||
value_getter=lambda s: s.victory_points,
|
lambda s: s.victory_points,
|
||||||
)
|
)
|
||||||
ties: List[TieContext] = []
|
ties: List[TieContext] = []
|
||||||
for _, group, _ in ranking:
|
for _, group, _ in ranking:
|
||||||
if len(group) <= 1:
|
if len(group) <= 1:
|
||||||
continue
|
continue
|
||||||
score_value = scores[group[0]].victory_points
|
score_value = scores[group[0]].victory_points
|
||||||
if TieResolver.is_tie_resolved(war, ContextType.WAR, war.id, score_value):
|
context: TieContext = TieContext(
|
||||||
|
ContextType.WAR,
|
||||||
|
war.id,
|
||||||
|
[],
|
||||||
|
score_value,
|
||||||
|
ScoreKind.VP,
|
||||||
|
)
|
||||||
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(war, ContextType.WAR, war.id, group):
|
if not TieResolver.can_tie_be_resolved(war, context, group):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(None, ContextType.WAR, war.id, score_value)
|
TieResolved(None, ContextType.WAR, war.id, score_value)
|
||||||
)
|
)
|
||||||
|
|
@ -170,6 +191,7 @@ class TieResolver:
|
||||||
context_id=war.id,
|
context_id=war.id,
|
||||||
participants=group,
|
participants=group,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
|
score_kind=ScoreKind.VP,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
@ -181,78 +203,62 @@ class TieResolver:
|
||||||
) -> List[TieContext]:
|
) -> List[TieContext]:
|
||||||
from warchron.model.result_checker import ResultChecker
|
from warchron.model.result_checker import ResultChecker
|
||||||
|
|
||||||
base_scores = ScoreService.compute_scores(
|
scores = ScoreService.compute_scores(
|
||||||
war,
|
war,
|
||||||
ContextType.WAR,
|
ContextType.WAR,
|
||||||
war.id,
|
war.id,
|
||||||
)
|
)
|
||||||
scores = TieResolver._build_objective_scores(
|
|
||||||
base_scores,
|
def value_getter(score: ParticipantScore) -> int:
|
||||||
objective_id,
|
return score.narrative_points.get(objective_id, 0)
|
||||||
)
|
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
ranking = ResultChecker.get_effective_ranking(
|
||||||
war,
|
war,
|
||||||
ContextType.OBJECTIVE,
|
ContextType.WAR,
|
||||||
f"{war.id}:{objective_id}",
|
war.id,
|
||||||
|
ScoreKind.NP,
|
||||||
scores,
|
scores,
|
||||||
value_getter=lambda s: s.narrative_points.get(objective_id, 0),
|
value_getter,
|
||||||
|
objective_id,
|
||||||
)
|
)
|
||||||
ties: List[TieContext] = []
|
ties: List[TieContext] = []
|
||||||
for _, group, _ in ranking:
|
for _, group, _ in ranking:
|
||||||
if len(group) <= 1:
|
if len(group) <= 1:
|
||||||
continue
|
continue
|
||||||
score_value = scores[group[0]].victory_points
|
np_value = value_getter(scores[group[0]])
|
||||||
context_id = f"{war.id}:{objective_id}"
|
context: TieContext = TieContext(
|
||||||
|
ContextType.WAR, war.id, [], np_value, ScoreKind.NP, objective_id
|
||||||
|
)
|
||||||
if TieResolver.is_tie_resolved(
|
if TieResolver.is_tie_resolved(
|
||||||
war,
|
war,
|
||||||
ContextType.OBJECTIVE,
|
context,
|
||||||
context_id,
|
|
||||||
score_value,
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war,
|
war,
|
||||||
ContextType.OBJECTIVE,
|
context,
|
||||||
context_id,
|
|
||||||
group,
|
group,
|
||||||
):
|
):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
TieResolved(None, ContextType.WAR, war.id, np_value, objective_id)
|
||||||
None,
|
|
||||||
ContextType.OBJECTIVE,
|
|
||||||
context_id,
|
|
||||||
score_value,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
TieContext(
|
TieContext(
|
||||||
context_type=ContextType.OBJECTIVE,
|
context_type=ContextType.WAR,
|
||||||
context_id=context_id,
|
context_id=war.id,
|
||||||
participants=group,
|
participants=group,
|
||||||
score_value=score_value,
|
score_value=np_value,
|
||||||
|
score_kind=ScoreKind.NP,
|
||||||
|
objective_id=objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_objective_scores(
|
|
||||||
base_scores: Dict[str, ParticipantScore],
|
|
||||||
objective_id: str,
|
|
||||||
) -> Dict[str, ParticipantScore]:
|
|
||||||
return {
|
|
||||||
pid: ParticipantScore(
|
|
||||||
victory_points=score.narrative_points.get(objective_id, 0),
|
|
||||||
narrative_points={},
|
|
||||||
)
|
|
||||||
for pid, score in base_scores.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_bids(
|
def apply_bids(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
bids: Dict[str, bool], # war_participant_id -> spend?
|
bids: Dict[str, bool], # war_participant_id -> spend?
|
||||||
) -> None:
|
) -> None:
|
||||||
for war_part_id, spend in bids.items():
|
for war_part_id, spend in bids.items():
|
||||||
|
|
@ -264,17 +270,17 @@ class TieResolver:
|
||||||
InfluenceSpent(
|
InfluenceSpent(
|
||||||
participant_id=war_part_id,
|
participant_id=war_part_id,
|
||||||
amount=1,
|
amount=1,
|
||||||
context_type=context_type,
|
context_type=context.context_type,
|
||||||
context_id=context_id,
|
context_id=context.context_id,
|
||||||
|
objective_id=context.objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# FIXME lost tokens used for narrative tie-break (ContextType.OBJECTIVE)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cancel_tie_break(
|
def cancel_tie_break(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
score_value: int | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
war.events = [
|
war.events = [
|
||||||
ev
|
ev
|
||||||
|
|
@ -282,14 +288,15 @@ class TieResolver:
|
||||||
if not (
|
if not (
|
||||||
(
|
(
|
||||||
isinstance(ev, InfluenceSpent)
|
isinstance(ev, InfluenceSpent)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
isinstance(ev, TieResolved)
|
isinstance(ev, TieResolved)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
and ev.score_value == score_value
|
and ev.score_value == context.score_value
|
||||||
|
and ev.objective_id == context.objective_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
@ -297,16 +304,16 @@ class TieResolver:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rank_by_tokens(
|
def rank_by_tokens(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
participants: List[str],
|
participants: List[str],
|
||||||
) -> List[List[str]]:
|
) -> List[List[str]]:
|
||||||
spent = {pid: 0 for pid in participants}
|
spent = {pid: 0 for pid in participants}
|
||||||
for ev in war.events:
|
for ev in war.events:
|
||||||
if (
|
if (
|
||||||
isinstance(ev, InfluenceSpent)
|
isinstance(ev, InfluenceSpent)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
|
and ev.objective_id == context.objective_id
|
||||||
and ev.participant_id in spent
|
and ev.participant_id in spent
|
||||||
):
|
):
|
||||||
spent[ev.participant_id] += ev.amount
|
spent[ev.participant_id] += ev.amount
|
||||||
|
|
@ -323,16 +330,16 @@ class TieResolver:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tokens_spent_map(
|
def tokens_spent_map(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
participants: List[str],
|
participants: List[str],
|
||||||
) -> Dict[str, int]:
|
) -> Dict[str, int]:
|
||||||
spent = {pid: 0 for pid in participants}
|
spent = {pid: 0 for pid in participants}
|
||||||
for ev in war.events:
|
for ev in war.events:
|
||||||
if (
|
if (
|
||||||
isinstance(ev, InfluenceSpent)
|
isinstance(ev, InfluenceSpent)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
|
and ev.objective_id == context.objective_id
|
||||||
and ev.participant_id in spent
|
and ev.participant_id in spent
|
||||||
):
|
):
|
||||||
spent[ev.participant_id] += ev.amount
|
spent[ev.participant_id] += ev.amount
|
||||||
|
|
@ -341,45 +348,57 @@ class TieResolver:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_active_participants(
|
def get_active_participants(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
participants: List[str],
|
participants: List[str],
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
groups = TieResolver.rank_by_tokens(war, context_type, context_id, participants)
|
groups = TieResolver.rank_by_tokens(war, context, participants)
|
||||||
return groups[0]
|
return groups[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_tie_state(
|
def resolve_tie_state(
|
||||||
war: War,
|
war: War,
|
||||||
ctx: TieContext,
|
context: TieContext,
|
||||||
bids: dict[str, bool] | None = None,
|
bids: dict[str, bool] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
active = TieResolver.get_active_participants(
|
active = TieResolver.get_active_participants(
|
||||||
war,
|
war,
|
||||||
ctx.context_type,
|
context,
|
||||||
ctx.context_id,
|
context.participants,
|
||||||
ctx.participants,
|
|
||||||
)
|
)
|
||||||
# confirmed draw if non had bid
|
# confirmed draw if non had bid
|
||||||
if not active:
|
if not active:
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
TieResolved(
|
||||||
|
None,
|
||||||
|
context.context_type,
|
||||||
|
context.context_id,
|
||||||
|
context.score_value,
|
||||||
|
context.objective_id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# confirmed draw if current bids are 0
|
# confirmed draw if current bids are 0
|
||||||
if bids is not None and not any(bids.values()):
|
if bids is not None and not any(bids.values()):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
TieResolved(
|
||||||
|
None,
|
||||||
|
context.context_type,
|
||||||
|
context.context_id,
|
||||||
|
context.score_value,
|
||||||
|
context.objective_id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# else rank_by_tokens
|
# else rank_by_tokens
|
||||||
groups = TieResolver.rank_by_tokens(
|
groups = TieResolver.rank_by_tokens(war, context, context.participants)
|
||||||
war, ctx.context_type, ctx.context_id, ctx.participants
|
|
||||||
)
|
|
||||||
if len(groups[0]) == 1:
|
if len(groups[0]) == 1:
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
TieResolved(
|
||||||
groups[0][0], ctx.context_type, ctx.context_id, ctx.score_value
|
groups[0][0],
|
||||||
|
context.context_type,
|
||||||
|
context.context_id,
|
||||||
|
context.score_value,
|
||||||
|
context.objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -387,41 +406,34 @@ class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_tie_be_resolved(
|
def can_tie_be_resolved(
|
||||||
war: War, context_type: ContextType, context_id: str, participants: List[str]
|
war: War, context: TieContext, participants: List[str]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
active = TieResolver.get_active_participants(
|
active = TieResolver.get_active_participants(war, context, participants)
|
||||||
war, context_type, context_id, participants
|
|
||||||
)
|
|
||||||
return any(war.get_influence_tokens(pid) > 0 for pid in active)
|
return any(war.get_influence_tokens(pid) > 0 for pid in active)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def was_tie_broken_by_tokens(
|
def was_tie_broken_by_tokens(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context: TieContext,
|
||||||
context_id: str,
|
|
||||||
score_value: int | None = None,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
for ev in reversed(war.events):
|
for ev in reversed(war.events):
|
||||||
if (
|
if (
|
||||||
isinstance(ev, TieResolved)
|
isinstance(ev, TieResolved)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
and ev.score_value == score_value
|
and ev.score_value == context.score_value
|
||||||
|
and ev.objective_id == context.objective_id
|
||||||
):
|
):
|
||||||
return ev.participant_id is not None
|
return ev.participant_id is not None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_tie_resolved(
|
def is_tie_resolved(war: War, context: TieContext) -> bool:
|
||||||
war: War,
|
|
||||||
context_type: ContextType,
|
|
||||||
context_id: str,
|
|
||||||
score_value: int | None = None,
|
|
||||||
) -> bool:
|
|
||||||
return any(
|
return any(
|
||||||
isinstance(ev, TieResolved)
|
isinstance(ev, TieResolved)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context.context_id
|
||||||
and ev.score_value == score_value
|
and ev.score_value == context.score_value
|
||||||
|
and ev.objective_id == context.objective_id
|
||||||
for ev in war.events
|
for ev in war.events
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ from typing import Dict, Any, TypeVar, Type, cast
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from warchron.model.json_helper import JsonHelper
|
||||||
|
|
||||||
T = TypeVar("T", bound="WarEvent")
|
T = TypeVar("T", bound="WarEvent")
|
||||||
EVENT_REGISTRY: Dict[str, Type["WarEvent"]] = {}
|
EVENT_REGISTRY: Dict[str, Type["WarEvent"]] = {}
|
||||||
|
|
||||||
|
|
@ -15,7 +17,12 @@ def register_event(cls: Type[T]) -> Type[T]:
|
||||||
class WarEvent:
|
class WarEvent:
|
||||||
TYPE = "WarEvent"
|
TYPE = "WarEvent"
|
||||||
|
|
||||||
def __init__(self, participant_id: str | None, context_type: str, context_id: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
participant_id: str | None,
|
||||||
|
context_type: str,
|
||||||
|
context_id: str,
|
||||||
|
):
|
||||||
self.id: str = str(uuid4())
|
self.id: str = str(uuid4())
|
||||||
self.participant_id: str | None = participant_id
|
self.participant_id: str | None = participant_id
|
||||||
self.context_type = context_type # battle, round, campaign, war
|
self.context_type = context_type # battle, round, campaign, war
|
||||||
|
|
@ -69,15 +76,18 @@ class TieResolved(WarEvent):
|
||||||
context_type: str,
|
context_type: str,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
score_value: int | None = None,
|
score_value: int | None = None,
|
||||||
|
objective_id: str | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.score_value = score_value
|
self.score_value = score_value
|
||||||
|
self.objective_id = objective_id
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
d.update(
|
d.update(
|
||||||
{
|
{
|
||||||
"score_value": self.score_value or None,
|
"score_value": self.score_value or None,
|
||||||
|
"objective_id": self.objective_id or None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
@ -85,10 +95,11 @@ class TieResolved(WarEvent):
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromDict(cls, data: Dict[str, Any]) -> TieResolved:
|
def fromDict(cls, data: Dict[str, Any]) -> TieResolved:
|
||||||
ev = cls(
|
ev = cls(
|
||||||
data["participant_id"] or None,
|
JsonHelper.none_if_empty(data["participant_id"]),
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
data["context_id"],
|
data["context_id"],
|
||||||
data["score_value"] or None,
|
JsonHelper.none_if_empty(data["score_value"]),
|
||||||
|
JsonHelper.none_if_empty(data["objective_id"]),
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
||||||
|
|
@ -98,9 +109,17 @@ class InfluenceGained(WarEvent):
|
||||||
TYPE = "InfluenceGained"
|
TYPE = "InfluenceGained"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, participant_id: str, amount: int, context_type: str, context_id: str
|
self,
|
||||||
|
participant_id: str,
|
||||||
|
amount: int,
|
||||||
|
context_type: str,
|
||||||
|
context_id: str,
|
||||||
):
|
):
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(
|
||||||
|
participant_id,
|
||||||
|
context_type,
|
||||||
|
context_id,
|
||||||
|
)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
|
|
@ -128,16 +147,23 @@ class InfluenceSpent(WarEvent):
|
||||||
TYPE = "InfluenceSpent"
|
TYPE = "InfluenceSpent"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, participant_id: str, amount: int, context_type: str, context_id: str
|
self,
|
||||||
|
participant_id: str,
|
||||||
|
amount: int,
|
||||||
|
context_type: str,
|
||||||
|
context_id: str,
|
||||||
|
objective_id: str | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
|
self.objective_id = objective_id
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
d.update(
|
d.update(
|
||||||
{
|
{
|
||||||
"amount": self.amount,
|
"amount": self.amount,
|
||||||
|
"objective_id": self.objective_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
@ -149,5 +175,6 @@ class InfluenceSpent(WarEvent):
|
||||||
int(data["amount"]),
|
int(data["amount"]),
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
data["context_id"],
|
data["context_id"],
|
||||||
|
JsonHelper.none_if_empty(data["objective_id"]),
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,12 @@ class TieDialog(QDialog):
|
||||||
def _get_context_title(
|
def _get_context_title(
|
||||||
context_type: ContextType, context_name: str | None = None
|
context_type: ContextType, context_name: str | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
|
if context_name:
|
||||||
|
return f"{context_name} tie"
|
||||||
titles = {
|
titles = {
|
||||||
ContextType.BATTLE: "Battle tie",
|
ContextType.BATTLE: "Battle tie",
|
||||||
ContextType.CAMPAIGN: "Campaign tie",
|
ContextType.CAMPAIGN: "Campaign tie",
|
||||||
ContextType.WAR: "War tie",
|
ContextType.WAR: "War tie",
|
||||||
ContextType.CHOICE: "Choice tie",
|
ContextType.CHOICE: "Choice tie",
|
||||||
ContextType.OBJECTIVE: f"Objective tie: {context_name}",
|
|
||||||
}
|
}
|
||||||
return titles.get(context_type, "Tie")
|
return titles.get(context_type, "Tie")
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,8 @@
|
||||||
"context_type": "battle",
|
"context_type": "battle",
|
||||||
"context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb",
|
"context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb",
|
||||||
"timestamp": "2026-02-26T16:11:44.346337",
|
"timestamp": "2026-02-26T16:11:44.346337",
|
||||||
"score_value": null
|
"score_value": null,
|
||||||
|
"objective_id": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_over": false
|
"is_over": false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue