fix round tie loop + improve tie ranking
This commit is contained in:
parent
60d8e6ca15
commit
f339498f97
6 changed files with 99 additions and 110 deletions
|
|
@ -96,6 +96,7 @@ class AppController:
|
||||||
self.navigation.refresh_players_view()
|
self.navigation.refresh_players_view()
|
||||||
self.navigation.refresh_wars_view()
|
self.navigation.refresh_wars_view()
|
||||||
self.update_window_title()
|
self.update_window_title()
|
||||||
|
# TODO refresh details view if wars tab selected
|
||||||
|
|
||||||
def open_file(self) -> None:
|
def open_file(self) -> None:
|
||||||
if self.is_dirty:
|
if self.is_dirty:
|
||||||
|
|
@ -116,6 +117,7 @@ class AppController:
|
||||||
self.navigation.refresh_players_view()
|
self.navigation.refresh_players_view()
|
||||||
self.navigation.refresh_wars_view()
|
self.navigation.refresh_wars_view()
|
||||||
self.update_window_title()
|
self.update_window_title()
|
||||||
|
# TODO refresh details view if wars tab selected
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
if not self.current_file:
|
if not self.current_file:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
from warchron.controller.app_controller import AppController
|
||||||
|
|
||||||
from warchron.model.war_event import TieResolved
|
|
||||||
from warchron.model.war import War
|
from warchron.model.war import War
|
||||||
from warchron.model.campaign import Campaign
|
from warchron.model.campaign import Campaign
|
||||||
from warchron.model.round import Round
|
from warchron.model.round import Round
|
||||||
|
|
@ -23,22 +22,8 @@ class RoundClosureWorkflow(ClosureWorkflow):
|
||||||
ClosureService.check_round_closable(round)
|
ClosureService.check_round_closable(round)
|
||||||
ties = TieResolver.find_battle_ties(war, round.id)
|
ties = TieResolver.find_battle_ties(war, round.id)
|
||||||
while ties:
|
while ties:
|
||||||
resolvable = []
|
bids_map = self.app.rounds.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
if TieResolver.can_tie_be_resolved(war, tie.participants):
|
|
||||||
resolvable.append(tie)
|
|
||||||
else:
|
|
||||||
war.events.append(
|
|
||||||
TieResolved(
|
|
||||||
participant_id=None, # draw confirmed
|
|
||||||
context_type=tie.context_type,
|
|
||||||
context_id=tie.context_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not resolvable:
|
|
||||||
break
|
|
||||||
bids_map = self.app.rounds.resolve_ties(war, resolvable)
|
|
||||||
for tie in resolvable:
|
|
||||||
bids = bids_map[tie.context_id]
|
bids = bids_map[tie.context_id]
|
||||||
TieResolver.apply_bids(
|
TieResolver.apply_bids(
|
||||||
war,
|
war,
|
||||||
|
|
@ -46,11 +31,8 @@ class RoundClosureWorkflow(ClosureWorkflow):
|
||||||
tie.context_id,
|
tie.context_id,
|
||||||
bids,
|
bids,
|
||||||
)
|
)
|
||||||
TieResolver.try_tie_break(
|
TieResolver.resolve_tie_state(
|
||||||
war,
|
war, tie.context_type, tie.context_id, tie.participants, bids
|
||||||
tie.context_type,
|
|
||||||
tie.context_id,
|
|
||||||
tie.participants,
|
|
||||||
)
|
)
|
||||||
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():
|
||||||
|
|
@ -64,22 +46,8 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
ClosureService.check_campaign_closable(campaign)
|
ClosureService.check_campaign_closable(campaign)
|
||||||
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
||||||
while ties:
|
while ties:
|
||||||
resolvable = []
|
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
if TieResolver.can_tie_be_resolved(war, tie.participants):
|
|
||||||
resolvable.append(tie)
|
|
||||||
else:
|
|
||||||
war.events.append(
|
|
||||||
TieResolved(
|
|
||||||
participant_id=None,
|
|
||||||
context_type=tie.context_type,
|
|
||||||
context_id=tie.context_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not resolvable:
|
|
||||||
break
|
|
||||||
bids_map = self.app.campaigns.resolve_ties(war, resolvable)
|
|
||||||
for tie in resolvable:
|
|
||||||
bids = bids_map[tie.context_id]
|
bids = bids_map[tie.context_id]
|
||||||
TieResolver.apply_bids(
|
TieResolver.apply_bids(
|
||||||
war,
|
war,
|
||||||
|
|
@ -87,7 +55,7 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
tie.context_id,
|
tie.context_id,
|
||||||
bids,
|
bids,
|
||||||
)
|
)
|
||||||
TieResolver.try_tie_break(
|
TieResolver.resolve_tie_state(
|
||||||
war,
|
war,
|
||||||
tie.context_type,
|
tie.context_type,
|
||||||
tie.context_id,
|
tie.context_id,
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ class Campaign:
|
||||||
mission: str | None,
|
mission: str | None,
|
||||||
description: str | None,
|
description: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# TODO raise error if sector used in a closed round (potential tokens)
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
raise ForbiddenOperation("Can't update sector in a closed campaign.")
|
raise ForbiddenOperation("Can't update sector in a closed campaign.")
|
||||||
sect = self.get_sector(sector_id)
|
sect = self.get_sector(sector_id)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ class ClosureService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None:
|
def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None:
|
||||||
already_granted = any(
|
already_granted = any(
|
||||||
isinstance(e, InfluenceGained) and e.source == f"battle:{battle.sector_id}"
|
isinstance(e, InfluenceGained)
|
||||||
|
and e.context_id == f"battle:{battle.sector_id}"
|
||||||
for e in war.events
|
for e in war.events
|
||||||
)
|
)
|
||||||
if already_granted:
|
if already_granted:
|
||||||
|
|
@ -49,7 +50,8 @@ class ClosureService:
|
||||||
InfluenceGained(
|
InfluenceGained(
|
||||||
participant_id=effective_winner,
|
participant_id=effective_winner,
|
||||||
amount=1,
|
amount=1,
|
||||||
source=f"battle:{battle.sector_id}",
|
context_type=ContextType.BATTLE,
|
||||||
|
context_id=f"battle:{battle.sector_id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ class TieContext:
|
||||||
participants: List[str] # war_participant_ids
|
participants: List[str] # war_participant_ids
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TieState:
|
||||||
|
winner: str | None
|
||||||
|
tied_players: List[str]
|
||||||
|
eliminated: List[str]
|
||||||
|
# is_final: bool
|
||||||
|
|
||||||
|
|
||||||
class TieResolver:
|
class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -26,20 +34,20 @@ 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
|
||||||
resolved = any(
|
if TieResolver.is_tie_resolved(war, ContextType.BATTLE, battle.sector_id):
|
||||||
isinstance(e, TieResolved)
|
|
||||||
and e.context_type == ContextType.BATTLE
|
|
||||||
and e.context_id == battle.sector_id
|
|
||||||
for e in war.events
|
|
||||||
)
|
|
||||||
if resolved:
|
|
||||||
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")
|
||||||
if battle.player_1_id is None or battle.player_2_id is None:
|
if battle.player_1_id is None or battle.player_2_id is None:
|
||||||
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(war, [p1, p2]):
|
||||||
|
war.events.append(
|
||||||
|
TieResolved(None, ContextType.BATTLE, battle.sector_id)
|
||||||
|
)
|
||||||
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
TieContext(
|
TieContext(
|
||||||
context_type=ContextType.BATTLE,
|
context_type=ContextType.BATTLE,
|
||||||
|
|
@ -51,13 +59,7 @@ class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_campaign_ties(war: War, campaign_id: str) -> List[TieContext]:
|
def find_campaign_ties(war: War, campaign_id: str) -> List[TieContext]:
|
||||||
resolved = any(
|
if TieResolver.is_tie_resolved(war, ContextType.CAMPAIGN, campaign_id):
|
||||||
isinstance(e, TieResolved)
|
|
||||||
and e.context_type == ContextType.CAMPAIGN
|
|
||||||
and e.context_id == campaign_id
|
|
||||||
for e in war.events
|
|
||||||
)
|
|
||||||
if resolved:
|
|
||||||
return []
|
return []
|
||||||
scores = ScoreService.compute_scores(war, ContextType.CAMPAIGN, campaign_id)
|
scores = ScoreService.compute_scores(war, ContextType.CAMPAIGN, campaign_id)
|
||||||
buckets: DefaultDict[int, List[str]] = defaultdict(list)
|
buckets: DefaultDict[int, List[str]] = defaultdict(list)
|
||||||
|
|
@ -96,46 +98,54 @@ class TieResolver:
|
||||||
participant_id=war_part_id,
|
participant_id=war_part_id,
|
||||||
amount=1,
|
amount=1,
|
||||||
context_type=context_type,
|
context_type=context_type,
|
||||||
|
context_id=context_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def try_tie_break(
|
def rank_by_tokens(
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context_type: ContextType,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
participants: List[str], # war_participant_ids
|
participants: List[str],
|
||||||
) -> bool:
|
) -> List[List[str]]:
|
||||||
spent: Dict[str, int] = {}
|
spent = {pid: 0 for pid in participants}
|
||||||
for war_part_id in participants:
|
for ev in war.events:
|
||||||
spent[war_part_id] = sum(
|
if (
|
||||||
e.amount
|
isinstance(ev, InfluenceSpent)
|
||||||
for e in war.events
|
and ev.context_type == context_type
|
||||||
if isinstance(e, InfluenceSpent)
|
and ev.context_id == context_id
|
||||||
and e.participant_id == war_part_id
|
and ev.participant_id in spent
|
||||||
and e.context_type == context_type
|
):
|
||||||
)
|
spent[ev.participant_id] += ev.amount
|
||||||
values = set(spent.values())
|
sorted_items = sorted(spent.items(), key=lambda x: x[1], reverse=True)
|
||||||
if values == {0}: # no bid = confirmed draw
|
groups: List[List[str]] = []
|
||||||
war.events.append(
|
current_score = None
|
||||||
TieResolved(
|
for pid, score in sorted_items:
|
||||||
participant_id=None,
|
if score != current_score:
|
||||||
context_type=context_type,
|
groups.append([])
|
||||||
context_id=context_id,
|
current_score = score
|
||||||
)
|
groups[-1].append(pid)
|
||||||
)
|
return groups
|
||||||
return True
|
|
||||||
if len(values) == 1: # tie again, continue
|
@staticmethod
|
||||||
return False
|
def resolve_tie_state(
|
||||||
winner = max(spent.items(), key=lambda item: item[1])[0]
|
war: War,
|
||||||
war.events.append(
|
context_type: ContextType,
|
||||||
TieResolved(
|
context_id: str,
|
||||||
participant_id=winner,
|
participants: List[str],
|
||||||
context_type=context_type,
|
bids: dict[str, bool] | None = None,
|
||||||
context_id=context_id,
|
) -> None:
|
||||||
)
|
# confirmed draw if bids are 0
|
||||||
)
|
if bids is not None and not any(bids.values()):
|
||||||
return True
|
war.events.append(TieResolved(None, context_type, context_id))
|
||||||
|
return
|
||||||
|
# else rank_by_tokens
|
||||||
|
groups = TieResolver.rank_by_tokens(war, context_type, context_id, participants)
|
||||||
|
if len(groups[0]) == 1:
|
||||||
|
war.events.append(TieResolved(groups[0][0], context_type, context_id))
|
||||||
|
return
|
||||||
|
# if tie persists, do nothing, workflow will call again
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_tie_be_resolved(war: War, participants: List[str]) -> bool:
|
def can_tie_be_resolved(war: War, participants: List[str]) -> bool:
|
||||||
|
|
@ -155,3 +165,12 @@ class TieResolver:
|
||||||
):
|
):
|
||||||
return ev.participant_id is not None
|
return ev.participant_id is not None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_tie_resolved(war: War, context_type: ContextType, context_id: str) -> bool:
|
||||||
|
return any(
|
||||||
|
isinstance(ev, TieResolved)
|
||||||
|
and ev.context_type == context_type
|
||||||
|
and ev.context_id == context_id
|
||||||
|
for ev in war.events
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ def register_event(cls: Type[T]) -> Type[T]:
|
||||||
class WarEvent:
|
class WarEvent:
|
||||||
TYPE = "WarEvent"
|
TYPE = "WarEvent"
|
||||||
|
|
||||||
def __init__(self, participant_id: str | None = None):
|
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_id = context_id
|
||||||
self.timestamp: datetime = datetime.now()
|
self.timestamp: datetime = datetime.now()
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
|
|
@ -34,6 +36,8 @@ class WarEvent:
|
||||||
"type": self.TYPE,
|
"type": self.TYPE,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"participant_id": self.participant_id,
|
"participant_id": self.participant_id,
|
||||||
|
"context_type": self.context_type,
|
||||||
|
"context_id": self.context_id,
|
||||||
"timestamp": self.timestamp.isoformat(),
|
"timestamp": self.timestamp.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +45,8 @@ class WarEvent:
|
||||||
def _base_fromDict(cls: Type[T], ev: T, data: Dict[str, Any]) -> T:
|
def _base_fromDict(cls: Type[T], ev: T, data: Dict[str, Any]) -> T:
|
||||||
ev.id = data["id"]
|
ev.id = data["id"]
|
||||||
ev.participant_id = data["participant_id"]
|
ev.participant_id = data["participant_id"]
|
||||||
|
ev.context_type = data["context_type"]
|
||||||
|
ev.context_id = data["context_id"]
|
||||||
ev.timestamp = datetime.fromisoformat(data["timestamp"])
|
ev.timestamp = datetime.fromisoformat(data["timestamp"])
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
|
|
@ -58,21 +64,10 @@ class TieResolved(WarEvent):
|
||||||
TYPE = "TieResolved"
|
TYPE = "TieResolved"
|
||||||
|
|
||||||
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):
|
||||||
super().__init__(participant_id)
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.participant_id: str | None = (
|
|
||||||
participant_id # winner or None (confirmed tie)
|
|
||||||
)
|
|
||||||
self.context_type = context_type # battle, round, campaign, war
|
|
||||||
self.context_id = context_id
|
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
d.update(
|
|
||||||
{
|
|
||||||
"context_type": self.context_type,
|
|
||||||
"context_id": self.context_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -89,17 +84,17 @@ class TieResolved(WarEvent):
|
||||||
class InfluenceGained(WarEvent):
|
class InfluenceGained(WarEvent):
|
||||||
TYPE = "InfluenceGained"
|
TYPE = "InfluenceGained"
|
||||||
|
|
||||||
def __init__(self, participant_id: str, amount: int, source: str):
|
def __init__(
|
||||||
super().__init__(participant_id)
|
self, participant_id: str, amount: int, context_type: str, context_id: str
|
||||||
|
):
|
||||||
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.source = source # "battle", "tie_resolution", etc.
|
|
||||||
|
|
||||||
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,
|
||||||
"source": self.source,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
@ -108,8 +103,9 @@ class InfluenceGained(WarEvent):
|
||||||
def fromDict(cls, data: Dict[str, Any]) -> InfluenceGained:
|
def fromDict(cls, data: Dict[str, Any]) -> InfluenceGained:
|
||||||
ev = cls(
|
ev = cls(
|
||||||
data["participant_id"],
|
data["participant_id"],
|
||||||
data["amount"],
|
int(data["amount"]),
|
||||||
data["source"],
|
data["context_type"],
|
||||||
|
data["context_id"],
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
||||||
|
|
@ -118,17 +114,17 @@ class InfluenceGained(WarEvent):
|
||||||
class InfluenceSpent(WarEvent):
|
class InfluenceSpent(WarEvent):
|
||||||
TYPE = "InfluenceSpent"
|
TYPE = "InfluenceSpent"
|
||||||
|
|
||||||
def __init__(self, participant_id: str, amount: int, context_type: str):
|
def __init__(
|
||||||
super().__init__(participant_id)
|
self, participant_id: str, amount: int, context_type: str, context_id: str
|
||||||
|
):
|
||||||
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.context_type = context_type # "battle_tie", "campaign_tie", etc.
|
|
||||||
|
|
||||||
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,
|
||||||
"context_type": self.context_type,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
@ -137,7 +133,8 @@ class InfluenceSpent(WarEvent):
|
||||||
def fromDict(cls, data: Dict[str, Any]) -> InfluenceSpent:
|
def fromDict(cls, data: Dict[str, Any]) -> InfluenceSpent:
|
||||||
ev = cls(
|
ev = cls(
|
||||||
data["participant_id"],
|
data["participant_id"],
|
||||||
data["amount"],
|
int(data["amount"]),
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
|
data["context_id"],
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue