Compare commits
No commits in common. "e64d9ff43b9670fbd8e7abd7b20c69e8d853d31a" and "789756d586fb0c48d381e357e1861cb61b1d67fb" have entirely different histories.
e64d9ff43b
...
789756d586
13 changed files with 169 additions and 386 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:
|
||||||
|
|
@ -213,13 +215,13 @@ class AppController:
|
||||||
self.navigation.refresh_and_select(
|
self.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id
|
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id
|
||||||
)
|
)
|
||||||
|
self.is_dirty = True
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.view,
|
self.view,
|
||||||
"Add forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except RequiresConfirmation as e:
|
except RequiresConfirmation as e:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self.view,
|
self.view,
|
||||||
|
|
@ -229,10 +231,7 @@ class AppController:
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
return
|
|
||||||
self.is_dirty = True
|
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
|
||||||
|
|
||||||
def edit_item(self, item_type: str, item_id: str) -> None:
|
def edit_item(self, item_type: str, item_id: str) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -267,13 +266,13 @@ class AppController:
|
||||||
elif item_type == ItemType.BATTLE:
|
elif item_type == ItemType.BATTLE:
|
||||||
self.rounds.edit_round_battle(item_id)
|
self.rounds.edit_round_battle(item_id)
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
self.is_dirty = True
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.view,
|
self.view,
|
||||||
"Update forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except RequiresConfirmation as e:
|
except RequiresConfirmation as e:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self.view,
|
self.view,
|
||||||
|
|
@ -283,10 +282,7 @@ class AppController:
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
return
|
|
||||||
self.is_dirty = True
|
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
|
||||||
|
|
||||||
def delete_item(self, item_type: str, item_id: str) -> None:
|
def delete_item(self, item_type: str, item_id: str) -> None:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
|
|
@ -330,13 +326,13 @@ class AppController:
|
||||||
self.navigation.refresh_and_select(
|
self.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id
|
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id
|
||||||
)
|
)
|
||||||
|
self.is_dirty = True
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.view,
|
self.view,
|
||||||
"Deletion forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except RequiresConfirmation as e:
|
except RequiresConfirmation as e:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self.view,
|
self.view,
|
||||||
|
|
@ -346,7 +342,4 @@ class AppController:
|
||||||
)
|
)
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
return
|
|
||||||
self.is_dirty = True
|
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
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 warchron.constants import (
|
from warchron.constants import (
|
||||||
RefreshScope,
|
RefreshScope,
|
||||||
ContextType,
|
ContextType,
|
||||||
ItemType,
|
ItemType,
|
||||||
|
Icons,
|
||||||
|
IconName,
|
||||||
|
RANK_TO_ICON,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -24,8 +28,8 @@ from warchron.model.campaign_participant import CampaignParticipant
|
||||||
from warchron.model.sector import Sector
|
from warchron.model.sector import Sector
|
||||||
from warchron.model.tie_manager import TieContext, TieResolver
|
from warchron.model.tie_manager import TieContext, TieResolver
|
||||||
from warchron.model.score_service import ScoreService
|
from warchron.model.score_service import ScoreService
|
||||||
|
from warchron.model.result_checker import ResultChecker
|
||||||
from warchron.controller.closure_workflow import CampaignClosureWorkflow
|
from warchron.controller.closure_workflow import CampaignClosureWorkflow
|
||||||
from warchron.controller.ranking_icon import RankingIcon
|
|
||||||
from warchron.view.campaign_dialog import CampaignDialog
|
from warchron.view.campaign_dialog import CampaignDialog
|
||||||
from warchron.view.campaign_participant_dialog import CampaignParticipantDialog
|
from warchron.view.campaign_participant_dialog import CampaignParticipantDialog
|
||||||
from warchron.view.sector_dialog import SectorDialog
|
from warchron.view.sector_dialog import SectorDialog
|
||||||
|
|
@ -36,6 +40,39 @@ class CampaignController:
|
||||||
def __init__(self, app: "AppController"):
|
def __init__(self, app: "AppController"):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
|
def _compute_campaign_ranking_icons(
|
||||||
|
self, war: War, campaign: Campaign
|
||||||
|
) -> Dict[str, QIcon]:
|
||||||
|
scores = ScoreService.compute_scores(
|
||||||
|
war,
|
||||||
|
ContextType.CAMPAIGN,
|
||||||
|
campaign.id,
|
||||||
|
)
|
||||||
|
ranking = ResultChecker.get_effective_ranking(
|
||||||
|
war, ContextType.CAMPAIGN, campaign.id, scores
|
||||||
|
)
|
||||||
|
icon_map = {}
|
||||||
|
for rank, group, token_map in ranking:
|
||||||
|
base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH)
|
||||||
|
tie_id = f"{campaign.id}:score:{scores[group[0]].victory_points}"
|
||||||
|
tie_resolved = TieResolver.is_tie_resolved(
|
||||||
|
war, ContextType.CAMPAIGN, tie_id
|
||||||
|
)
|
||||||
|
for pid in group:
|
||||||
|
spent = token_map.get(pid, 0)
|
||||||
|
if not tie_resolved and spent == 0:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}DRAW")
|
||||||
|
elif tie_resolved and spent == 0 and len(group) > 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}DRAW")
|
||||||
|
elif tie_resolved and spent > 0 and len(group) == 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}BREAK")
|
||||||
|
elif tie_resolved and spent > 0 and len(group) > 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW")
|
||||||
|
else:
|
||||||
|
icon_name = base_icon
|
||||||
|
icon_map[pid] = QIcon(Icons.get_pixmap(icon_name))
|
||||||
|
return icon_map
|
||||||
|
|
||||||
def _fill_campaign_details(self, campaign_id: str) -> None:
|
def _fill_campaign_details(self, campaign_id: str) -> None:
|
||||||
camp = self.app.model.get_campaign(campaign_id)
|
camp = self.app.model.get_campaign(campaign_id)
|
||||||
self.app.view.show_campaign_details(name=camp.name, month=camp.month)
|
self.app.view.show_campaign_details(name=camp.name, month=camp.month)
|
||||||
|
|
@ -59,9 +96,7 @@ class CampaignController:
|
||||||
rows: List[CampaignParticipantScoreDTO] = []
|
rows: List[CampaignParticipantScoreDTO] = []
|
||||||
icon_map = {}
|
icon_map = {}
|
||||||
if camp.is_over:
|
if camp.is_over:
|
||||||
icon_map = RankingIcon.compute_icons(
|
icon_map = self._compute_campaign_ranking_icons(war, camp)
|
||||||
war, ContextType.CAMPAIGN, campaign_id, scores
|
|
||||||
)
|
|
||||||
for camp_part in camp.get_all_campaign_participants():
|
for camp_part in camp.get_all_campaign_participants():
|
||||||
war_part_id = camp_part.war_participant_id
|
war_part_id = camp_part.war_participant_id
|
||||||
war_part = war.get_war_participant(war_part_id)
|
war_part = war.get_war_participant(war_part_id)
|
||||||
|
|
@ -142,10 +177,9 @@ class CampaignController:
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.app.view,
|
self.app.view,
|
||||||
"Closure forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
|
|
@ -154,7 +188,7 @@ 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[str, 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(
|
||||||
|
|
@ -173,17 +207,11 @@ class CampaignController:
|
||||||
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, ContextType.CAMPAIGN, ctx.context_id)
|
||||||
war, ContextType.CAMPAIGN, 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.context_id] = dialog.get_bids()
|
||||||
dialog.get_bids()
|
|
||||||
)
|
|
||||||
return bids_map
|
return bids_map
|
||||||
|
|
||||||
# Campaign participant methods
|
|
||||||
|
|
||||||
def create_campaign_participant(self) -> CampaignParticipant | None:
|
def create_campaign_participant(self) -> CampaignParticipant | None:
|
||||||
if not self.app.navigation.selected_campaign_id:
|
if not self.app.navigation.selected_campaign_id:
|
||||||
return None
|
return None
|
||||||
|
|
@ -242,6 +270,7 @@ class CampaignController:
|
||||||
self.app.view, "Invalid name", "Sector name cannot be empty."
|
self.app.view, "Invalid name", "Sector name cannot be empty."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
# TODO allow same objectives in different fields?
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_sector(self) -> Sector | None:
|
def create_sector(self) -> Sector | None:
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,11 @@ 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.context_id]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(
|
||||||
|
war, tie.context_type, tie.context_id, tie.participants, 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():
|
||||||
ClosureService.apply_battle_outcomes(war, campaign, battle)
|
ClosureService.apply_battle_outcomes(war, campaign, battle)
|
||||||
|
|
@ -41,9 +43,11 @@ 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.context_id]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(
|
||||||
|
war, tie.context_type, tie.context_id, tie.participants, bids
|
||||||
|
)
|
||||||
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
||||||
ClosureService.finalize_campaign(campaign)
|
ClosureService.finalize_campaign(campaign)
|
||||||
|
|
||||||
|
|
@ -56,8 +60,10 @@ 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.context_id]
|
||||||
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids)
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.resolve_tie_state(
|
||||||
|
war, tie.context_type, tie.context_id, tie.participants, bids
|
||||||
|
)
|
||||||
ties = TieResolver.find_war_ties(war)
|
ties = TieResolver.find_war_ties(war)
|
||||||
ClosureService.finalize_war(war)
|
ClosureService.finalize_war(war)
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,7 @@ class NavigationController:
|
||||||
self.app.view.display_players(players_for_display)
|
self.app.view.display_players(players_for_display)
|
||||||
|
|
||||||
def refresh_wars_view(self) -> None:
|
def refresh_wars_view(self) -> None:
|
||||||
wars = self.app.model.get_all_wars()
|
wars: List[WarDTO] = [
|
||||||
wars_dto: List[WarDTO] = [
|
|
||||||
WarDTO(
|
WarDTO(
|
||||||
id=w.id,
|
id=w.id,
|
||||||
name=w.name,
|
name=w.name,
|
||||||
|
|
@ -58,21 +57,7 @@ class NavigationController:
|
||||||
)
|
)
|
||||||
for w in self.app.model.get_all_wars()
|
for w in self.app.model.get_all_wars()
|
||||||
]
|
]
|
||||||
self.app.view.display_wars_tree(wars_dto)
|
self.app.view.display_wars_tree(wars)
|
||||||
if not wars:
|
|
||||||
self.clear_selection()
|
|
||||||
return
|
|
||||||
first_war = wars[0]
|
|
||||||
self.selected_war_id = first_war.id
|
|
||||||
self.selected_campaign_id = None
|
|
||||||
self.selected_round_id = None
|
|
||||||
self.app.view.select_tree_item(
|
|
||||||
item_type=ItemType.WAR,
|
|
||||||
item_id=first_war.id,
|
|
||||||
)
|
|
||||||
self.app.view.show_details(ItemType.WAR)
|
|
||||||
self.app.wars._fill_war_details(first_war.id)
|
|
||||||
self.update_actions_state()
|
|
||||||
|
|
||||||
def refresh(self, scope: RefreshScope) -> None:
|
def refresh(self, scope: RefreshScope) -> None:
|
||||||
match scope:
|
match scope:
|
||||||
|
|
@ -102,13 +87,6 @@ class NavigationController:
|
||||||
|
|
||||||
# Commands methods
|
# Commands methods
|
||||||
|
|
||||||
def clear_selection(self) -> None:
|
|
||||||
self.selected_war_id = None
|
|
||||||
self.selected_campaign_id = None
|
|
||||||
self.selected_round_id = None
|
|
||||||
self.app.view.clear_tree_selection()
|
|
||||||
self.app.view.show_details(None)
|
|
||||||
|
|
||||||
def on_tree_selection_changed(self, selection: TreeSelection | None) -> None:
|
def on_tree_selection_changed(self, selection: TreeSelection | None) -> None:
|
||||||
self.selected_war_id = None
|
self.selected_war_id = None
|
||||||
self.selected_campaign_id = None
|
self.selected_campaign_id = None
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from PyQt6.QtGui import QIcon
|
|
||||||
|
|
||||||
from warchron.constants import (
|
|
||||||
ContextType,
|
|
||||||
Icons,
|
|
||||||
IconName,
|
|
||||||
RANK_TO_ICON,
|
|
||||||
)
|
|
||||||
from warchron.model.war import War
|
|
||||||
from warchron.model.score_service import ParticipantScore
|
|
||||||
from warchron.model.result_checker import ResultChecker
|
|
||||||
|
|
||||||
|
|
||||||
class RankingIcon:
|
|
||||||
@staticmethod
|
|
||||||
def compute_icons(
|
|
||||||
war: War,
|
|
||||||
context_type: ContextType,
|
|
||||||
context_id: str,
|
|
||||||
scores: Dict[str, ParticipantScore],
|
|
||||||
) -> Dict[str, QIcon]:
|
|
||||||
# scores = ScoreService.compute_scores(
|
|
||||||
# war,
|
|
||||||
# context_type,
|
|
||||||
# context_id,
|
|
||||||
# )
|
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
|
||||||
war, context_type, context_id, scores
|
|
||||||
)
|
|
||||||
icon_map = {}
|
|
||||||
for rank, group, token_map in ranking:
|
|
||||||
base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH)
|
|
||||||
vp = scores[group[0]].victory_points
|
|
||||||
original_group_size = sum(
|
|
||||||
1 for s in scores.values() if s.victory_points == vp
|
|
||||||
)
|
|
||||||
for pid in group:
|
|
||||||
spent = token_map.get(pid, 0)
|
|
||||||
if original_group_size == 1:
|
|
||||||
icon_name = base_icon
|
|
||||||
elif len(group) == 1:
|
|
||||||
if spent > 0:
|
|
||||||
icon_name = getattr(IconName, f"{base_icon.name}BREAK")
|
|
||||||
else:
|
|
||||||
icon_name = base_icon
|
|
||||||
else:
|
|
||||||
if spent > 0:
|
|
||||||
icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW")
|
|
||||||
else:
|
|
||||||
icon_name = getattr(IconName, f"{base_icon.name}DRAW")
|
|
||||||
icon_map[pid] = QIcon(Icons.get_pixmap(icon_name))
|
|
||||||
return icon_map
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -167,10 +167,9 @@ class RoundController:
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.app.view,
|
self.app.view,
|
||||||
"Closure forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
|
|
@ -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[str, Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
players = [
|
players = [
|
||||||
|
|
@ -198,13 +197,8 @@ class RoundController:
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
TieResolver.cancel_tie_break(
|
|
||||||
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.context_id] = dialog.get_bids()
|
||||||
dialog.get_bids()
|
|
||||||
)
|
|
||||||
return bids_map
|
return bids_map
|
||||||
|
|
||||||
# Choice methods
|
# Choice methods
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
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 warchron.constants import (
|
from warchron.constants import (
|
||||||
RefreshScope,
|
RefreshScope,
|
||||||
ItemType,
|
ItemType,
|
||||||
ContextType,
|
ContextType,
|
||||||
|
Icons,
|
||||||
|
IconName,
|
||||||
|
RANK_TO_ICON,
|
||||||
)
|
)
|
||||||
from warchron.model.exception import (
|
from warchron.model.exception import DomainError, ForbiddenOperation
|
||||||
DomainError,
|
|
||||||
ForbiddenOperation,
|
|
||||||
RequiresConfirmation,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
from warchron.controller.app_controller import AppController
|
||||||
|
|
@ -25,8 +25,8 @@ from warchron.model.war_participant import WarParticipant
|
||||||
from warchron.model.objective import Objective
|
from warchron.model.objective import Objective
|
||||||
from warchron.model.tie_manager import TieContext, TieResolver
|
from warchron.model.tie_manager import TieContext, TieResolver
|
||||||
from warchron.model.score_service import ScoreService
|
from warchron.model.score_service import ScoreService
|
||||||
|
from warchron.model.result_checker import ResultChecker
|
||||||
from warchron.controller.closure_workflow import WarClosureWorkflow
|
from warchron.controller.closure_workflow import WarClosureWorkflow
|
||||||
from warchron.controller.ranking_icon import RankingIcon
|
|
||||||
from warchron.view.war_dialog import WarDialog
|
from warchron.view.war_dialog import WarDialog
|
||||||
from warchron.view.objective_dialog import ObjectiveDialog
|
from warchron.view.objective_dialog import ObjectiveDialog
|
||||||
from warchron.view.war_participant_dialog import WarParticipantDialog
|
from warchron.view.war_participant_dialog import WarParticipantDialog
|
||||||
|
|
@ -37,6 +37,35 @@ class WarController:
|
||||||
def __init__(self, app: "AppController"):
|
def __init__(self, app: "AppController"):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
|
def _compute_war_ranking_icons(self, war: War) -> Dict[str, QIcon]:
|
||||||
|
scores = ScoreService.compute_scores(
|
||||||
|
war,
|
||||||
|
ContextType.WAR,
|
||||||
|
war.id,
|
||||||
|
)
|
||||||
|
ranking = ResultChecker.get_effective_ranking(
|
||||||
|
war, ContextType.WAR, war.id, scores
|
||||||
|
)
|
||||||
|
icon_map = {}
|
||||||
|
for rank, group, token_map in ranking:
|
||||||
|
base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH)
|
||||||
|
tie_id = f"{war.id}:score:{scores[group[0]].victory_points}"
|
||||||
|
tie_resolved = TieResolver.is_tie_resolved(war, ContextType.WAR, tie_id)
|
||||||
|
for pid in group:
|
||||||
|
spent = token_map.get(pid, 0)
|
||||||
|
if not tie_resolved and spent == 0:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}DRAW")
|
||||||
|
elif tie_resolved and spent == 0 and len(group) > 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}DRAW")
|
||||||
|
elif tie_resolved and spent > 0 and len(group) == 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}BREAK")
|
||||||
|
elif tie_resolved and spent > 0 and len(group) > 1:
|
||||||
|
icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW")
|
||||||
|
else:
|
||||||
|
icon_name = base_icon
|
||||||
|
icon_map[pid] = QIcon(Icons.get_pixmap(icon_name))
|
||||||
|
return icon_map
|
||||||
|
|
||||||
def _fill_war_details(self, war_id: str) -> None:
|
def _fill_war_details(self, war_id: str) -> None:
|
||||||
war = self.app.model.get_war(war_id)
|
war = self.app.model.get_war(war_id)
|
||||||
self.app.view.show_war_details(name=war.name, year=war.year)
|
self.app.view.show_war_details(name=war.name, year=war.year)
|
||||||
|
|
@ -55,7 +84,7 @@ class WarController:
|
||||||
rows: List[WarParticipantScoreDTO] = []
|
rows: List[WarParticipantScoreDTO] = []
|
||||||
icon_map = {}
|
icon_map = {}
|
||||||
if war.is_over:
|
if war.is_over:
|
||||||
icon_map = RankingIcon.compute_icons(war, ContextType.WAR, war_id, scores)
|
icon_map = self._compute_war_ranking_icons(war)
|
||||||
for war_part in war.get_all_war_participants():
|
for war_part in war.get_all_war_participants():
|
||||||
player_name = self.app.model.get_player_name(war_part.player_id)
|
player_name = self.app.model.get_player_name(war_part.player_id)
|
||||||
score = scores[war_part.id]
|
score = scores[war_part.id]
|
||||||
|
|
@ -123,26 +152,23 @@ class WarController:
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self.app.view,
|
self.app.view,
|
||||||
"Closure forbidden",
|
"Deletion forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO fix ignored campaign tie-breaks
|
||||||
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[str, 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.context_id, ctx.participants
|
||||||
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))
|
||||||
|
|
@ -157,13 +183,9 @@ class WarController:
|
||||||
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, ContextType.WAR, ctx.context_id)
|
||||||
war, ContextType.WAR, 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.context_id] = 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:
|
||||||
|
|
@ -178,7 +200,6 @@ class WarController:
|
||||||
"Setting forbidden",
|
"Setting forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
|
||||||
|
|
@ -194,7 +215,6 @@ class WarController:
|
||||||
"Setting forbidden",
|
"Setting forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
|
||||||
|
|
@ -210,23 +230,8 @@ class WarController:
|
||||||
"Setting forbidden",
|
"Setting forbidden",
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
except RequiresConfirmation as e:
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self.app.view,
|
|
||||||
"Confirm update",
|
|
||||||
str(e),
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
)
|
|
||||||
if reply == QMessageBox.StandardButton.Yes:
|
|
||||||
e.action()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Objective methods
|
# Objective methods
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ class ClosureService:
|
||||||
from warchron.model.result_checker import ResultChecker
|
from warchron.model.result_checker import ResultChecker
|
||||||
|
|
||||||
already_granted = any(
|
already_granted = any(
|
||||||
isinstance(e, InfluenceGained) and e.context_id == 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:
|
||||||
|
|
@ -50,7 +51,7 @@ class ClosureService:
|
||||||
participant_id=effective_winner,
|
participant_id=effective_winner,
|
||||||
amount=1,
|
amount=1,
|
||||||
context_type=ContextType.BATTLE,
|
context_type=ContextType.BATTLE,
|
||||||
context_id=battle.sector_id,
|
context_id=f"battle:{battle.sector_id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,42 +45,10 @@ class ResultChecker:
|
||||||
current_rank = 1
|
current_rank = 1
|
||||||
for vp in sorted_vps:
|
for vp in sorted_vps:
|
||||||
participants = vp_buckets[vp]
|
participants = vp_buckets[vp]
|
||||||
if context_type == ContextType.WAR and len(participants) > 1:
|
tie_id = f"{context_id}:score:{vp}"
|
||||||
subgroups = ResultChecker._secondary_sorting_war(war, participants)
|
|
||||||
for subgroup in subgroups:
|
|
||||||
# no tie if campaigns' rank is enough to sort
|
|
||||||
if len(subgroup) == 1:
|
|
||||||
ranking.append(
|
|
||||||
(current_rank, subgroup, {pid: 0 for pid in subgroup})
|
|
||||||
)
|
|
||||||
current_rank += 1
|
|
||||||
continue
|
|
||||||
# normal tie-break if tie persists
|
|
||||||
if not TieResolver.is_tie_resolved(
|
|
||||||
war, context_type, context_id, vp
|
|
||||||
):
|
|
||||||
ranking.append(
|
|
||||||
(current_rank, subgroup, {pid: 0 for pid in subgroup})
|
|
||||||
)
|
|
||||||
current_rank += 1
|
|
||||||
continue
|
|
||||||
groups = TieResolver.rank_by_tokens(
|
|
||||||
war,
|
|
||||||
context_type,
|
|
||||||
context_id,
|
|
||||||
subgroup,
|
|
||||||
)
|
|
||||||
tokens_spent = TieResolver.tokens_spent_map(
|
|
||||||
war, context_type, context_id, subgroup
|
|
||||||
)
|
|
||||||
for group in groups:
|
|
||||||
group_tokens = {pid: tokens_spent[pid] for pid in group}
|
|
||||||
ranking.append((current_rank, group, group_tokens))
|
|
||||||
current_rank += 1
|
|
||||||
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_type, context_id, vp
|
war, context_type, tie_id
|
||||||
):
|
):
|
||||||
ranking.append(
|
ranking.append(
|
||||||
(current_rank, participants, {pid: 0 for pid in participants})
|
(current_rank, participants, {pid: 0 for pid in participants})
|
||||||
|
|
@ -91,46 +59,14 @@ class ResultChecker:
|
||||||
groups = TieResolver.rank_by_tokens(
|
groups = TieResolver.rank_by_tokens(
|
||||||
war,
|
war,
|
||||||
context_type,
|
context_type,
|
||||||
context_id,
|
tie_id,
|
||||||
participants,
|
participants,
|
||||||
)
|
)
|
||||||
tokens_spent = TieResolver.tokens_spent_map(
|
tokens_spent = TieResolver.tokens_spent_map(
|
||||||
war, context_type, context_id, participants
|
war, context_type, tie_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))
|
||||||
current_rank += 1
|
current_rank += 1
|
||||||
return ranking
|
return ranking
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _secondary_sorting_war(
|
|
||||||
war: War,
|
|
||||||
participants: List[str],
|
|
||||||
) -> List[List[str]]:
|
|
||||||
from warchron.model.score_service import ScoreService
|
|
||||||
|
|
||||||
rank_map: Dict[str, Tuple[int, ...]] = {}
|
|
||||||
for pid in participants:
|
|
||||||
ranks: List[int] = []
|
|
||||||
for campaign in war.campaigns:
|
|
||||||
scores = ScoreService.compute_scores(
|
|
||||||
war, ContextType.CAMPAIGN, campaign.id
|
|
||||||
)
|
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
|
||||||
war, ContextType.CAMPAIGN, campaign.id, scores
|
|
||||||
)
|
|
||||||
for rank, group, _ in ranking:
|
|
||||||
if pid in group:
|
|
||||||
ranks.append(rank)
|
|
||||||
break
|
|
||||||
rank_map[pid] = tuple(ranks)
|
|
||||||
sorted_items = sorted(rank_map.items(), key=lambda x: x[1])
|
|
||||||
groups: List[List[str]] = []
|
|
||||||
current_tuple = None
|
|
||||||
for pid, rank_tuple in sorted_items:
|
|
||||||
if rank_tuple != current_tuple:
|
|
||||||
groups.append([])
|
|
||||||
current_tuple = rank_tuple
|
|
||||||
groups[-1].append(pid)
|
|
||||||
return groups
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ class TieContext:
|
||||||
context_type: ContextType
|
context_type: ContextType
|
||||||
context_id: str
|
context_id: str
|
||||||
participants: List[str] # war_participant_ids
|
participants: List[str] # war_participant_ids
|
||||||
score_value: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class TieResolver:
|
class TieResolver:
|
||||||
|
|
@ -27,10 +26,9 @@ 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(
|
if TieResolver.is_tie_resolved(war, ContextType.BATTLE, battle.sector_id):
|
||||||
war, ContextType.BATTLE, battle.sector_id, None
|
|
||||||
):
|
|
||||||
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:
|
||||||
|
|
@ -49,13 +47,14 @@ class TieResolver:
|
||||||
context_type=ContextType.BATTLE,
|
context_type=ContextType.BATTLE,
|
||||||
context_id=battle.sector_id,
|
context_id=battle.sector_id,
|
||||||
participants=[p1, p2],
|
participants=[p1, p2],
|
||||||
score_value=None,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_campaign_ties(war: War, campaign_id: str) -> List[TieContext]:
|
def find_campaign_ties(war: War, campaign_id: str) -> List[TieContext]:
|
||||||
|
if TieResolver.is_tie_resolved(war, ContextType.CAMPAIGN, campaign_id):
|
||||||
|
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)
|
||||||
for pid, score in scores.items():
|
for pid, score in scores.items():
|
||||||
|
|
@ -64,53 +63,48 @@ 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(
|
tie_id = f"{campaign_id}:score:{score_value}"
|
||||||
war, ContextType.CAMPAIGN, campaign_id, score_value
|
if TieResolver.is_tie_resolved(war, ContextType.CAMPAIGN, tie_id):
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war, ContextType.CAMPAIGN, campaign_id, participants
|
war, ContextType.CAMPAIGN, tie_id, participants
|
||||||
):
|
):
|
||||||
war.events.append(
|
war.events.append(TieResolved(None, ContextType.CAMPAIGN, tie_id))
|
||||||
TieResolved(None, ContextType.CAMPAIGN, campaign_id, score_value)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
TieContext(
|
TieContext(
|
||||||
context_type=ContextType.CAMPAIGN,
|
context_type=ContextType.CAMPAIGN,
|
||||||
context_id=campaign_id,
|
context_id=tie_id,
|
||||||
participants=participants,
|
participants=participants,
|
||||||
score_value=score_value,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_war_ties(war: War) -> List[TieContext]:
|
def find_war_ties(war: War) -> List[TieContext]:
|
||||||
from warchron.model.result_checker import ResultChecker
|
if TieResolver.is_tie_resolved(war, ContextType.WAR, war.id):
|
||||||
|
return []
|
||||||
scores = ScoreService.compute_scores(war, ContextType.WAR, war.id)
|
scores = ScoreService.compute_scores(war, ContextType.WAR, war.id)
|
||||||
ranking = ResultChecker.get_effective_ranking(
|
buckets: DefaultDict[int, List[str]] = defaultdict(list)
|
||||||
war, ContextType.WAR, war.id, scores
|
for pid, score in scores.items():
|
||||||
)
|
buckets[score.victory_points].append(pid)
|
||||||
ties: List[TieContext] = []
|
ties: List[TieContext] = []
|
||||||
for _, group, _ in ranking:
|
for score_value, participants in buckets.items():
|
||||||
if len(group) <= 1:
|
if len(participants) <= 1:
|
||||||
continue
|
continue
|
||||||
score_value = scores[group[0]].victory_points
|
tie_id = f"{war.id}:score:{score_value}"
|
||||||
if TieResolver.is_tie_resolved(war, ContextType.WAR, war.id, score_value):
|
if TieResolver.is_tie_resolved(war, ContextType.WAR, tie_id):
|
||||||
continue
|
continue
|
||||||
if not TieResolver.can_tie_be_resolved(war, ContextType.WAR, war.id, group):
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war.events.append(
|
war, ContextType.WAR, tie_id, participants
|
||||||
TieResolved(None, ContextType.WAR, war.id, score_value)
|
):
|
||||||
)
|
war.events.append(TieResolved(None, ContextType.WAR, tie_id))
|
||||||
continue
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
TieContext(
|
TieContext(
|
||||||
context_type=ContextType.WAR,
|
context_type=ContextType.WAR,
|
||||||
context_id=war.id,
|
context_id=tie_id,
|
||||||
participants=group,
|
participants=participants,
|
||||||
score_value=score_value,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return ties
|
return ties
|
||||||
|
|
@ -141,7 +135,6 @@ class TieResolver:
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context_type: ContextType,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
score_value: int | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
war.events = [
|
war.events = [
|
||||||
ev
|
ev
|
||||||
|
|
@ -156,7 +149,6 @@ class TieResolver:
|
||||||
isinstance(ev, TieResolved)
|
isinstance(ev, TieResolved)
|
||||||
and ev.context_type == context_type
|
and ev.context_type == context_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context_id
|
||||||
and ev.score_value == score_value
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
@ -218,37 +210,26 @@ class TieResolver:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_tie_state(
|
def resolve_tie_state(
|
||||||
war: War,
|
war: War,
|
||||||
ctx: TieContext,
|
context_type: ContextType,
|
||||||
|
context_id: str,
|
||||||
|
participants: List[str],
|
||||||
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, context_type, context_id, participants
|
||||||
ctx.context_type,
|
|
||||||
ctx.context_id,
|
|
||||||
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, context_type, context_id))
|
||||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
|
||||||
)
|
|
||||||
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, context_type, context_id))
|
||||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
# else rank_by_tokens
|
# else rank_by_tokens
|
||||||
groups = TieResolver.rank_by_tokens(
|
groups = TieResolver.rank_by_tokens(war, context_type, context_id, 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(groups[0][0], context_type, context_id))
|
||||||
TieResolved(
|
|
||||||
groups[0][0], ctx.context_type, ctx.context_id, ctx.score_value
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
# if tie persists, do nothing, workflow will call again
|
# if tie persists, do nothing, workflow will call again
|
||||||
|
|
||||||
|
|
@ -266,29 +247,21 @@ class TieResolver:
|
||||||
war: War,
|
war: War,
|
||||||
context_type: ContextType,
|
context_type: ContextType,
|
||||||
context_id: str,
|
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_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context_id
|
||||||
and ev.score_value == score_value
|
|
||||||
):
|
):
|
||||||
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_type: ContextType, context_id: str) -> 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_type
|
||||||
and ev.context_id == context_id
|
and ev.context_id == context_id
|
||||||
and ev.score_value == score_value
|
|
||||||
for ev in war.events
|
for ev in war.events
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,8 @@ from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from warchron.constants import ContextType
|
from warchron.model.war_event import WarEvent, InfluenceGained, InfluenceSpent
|
||||||
from warchron.model.war_event import (
|
from warchron.model.exception import ForbiddenOperation
|
||||||
WarEvent,
|
|
||||||
InfluenceGained,
|
|
||||||
InfluenceSpent,
|
|
||||||
TieResolved,
|
|
||||||
)
|
|
||||||
from warchron.model.exception import ForbiddenOperation, RequiresConfirmation
|
|
||||||
from warchron.model.war_participant import WarParticipant
|
from warchron.model.war_participant import WarParticipant
|
||||||
from warchron.model.objective import Objective
|
from warchron.model.objective import Objective
|
||||||
from warchron.model.campaign_participant import CampaignParticipant
|
from warchron.model.campaign_participant import CampaignParticipant
|
||||||
|
|
@ -61,66 +55,10 @@ class War:
|
||||||
def set_influence_token(self, new_state: bool) -> None:
|
def set_influence_token(self, new_state: bool) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
raise ForbiddenOperation("Can't set influence token of a closed war.")
|
raise ForbiddenOperation("Can't set influence token of a closed war.")
|
||||||
|
# TODO raise RequiresConfirmation
|
||||||
def cleanup_token_and_tie() -> None:
|
# * disable: cleanup if any token has already been gained/spent
|
||||||
new_events: List[WarEvent] = []
|
# * enable: retrigger battle_outcomes and resolve tie again if any draw
|
||||||
for ev in self.events:
|
self.influence_token = new_state
|
||||||
if isinstance(ev, (InfluenceSpent, InfluenceGained)):
|
|
||||||
continue
|
|
||||||
if isinstance(ev, TieResolved):
|
|
||||||
ev.set_participant(None)
|
|
||||||
new_events.append(ev)
|
|
||||||
self.events = new_events
|
|
||||||
self.influence_token = new_state
|
|
||||||
|
|
||||||
def reset_tie_break() -> None:
|
|
||||||
new_events: List[WarEvent] = []
|
|
||||||
for ev in self.events:
|
|
||||||
if isinstance(ev, (TieResolved)):
|
|
||||||
if ev.context_type == ContextType.BATTLE:
|
|
||||||
battle = self.get_battle(ev.context_id)
|
|
||||||
campaign = self.get_campaign_by_sector(battle.sector_id)
|
|
||||||
round = campaign.get_round_by_battle(ev.context_id)
|
|
||||||
round.is_over = False
|
|
||||||
elif ev.context_type == ContextType.CAMPAIGN:
|
|
||||||
campaign = self.get_campaign(ev.context_id)
|
|
||||||
campaign.is_over = False
|
|
||||||
else:
|
|
||||||
new_events.append(ev)
|
|
||||||
self.events = new_events
|
|
||||||
for camp in self.campaigns:
|
|
||||||
for sect in camp.get_all_sectors():
|
|
||||||
if sect.influence_objective_id is not None:
|
|
||||||
round = camp.get_round_by_battle(sect.id)
|
|
||||||
round.is_over = False
|
|
||||||
self.influence_token = new_state
|
|
||||||
|
|
||||||
if new_state is False:
|
|
||||||
if not self.events:
|
|
||||||
self.influence_token = new_state
|
|
||||||
return
|
|
||||||
raise RequiresConfirmation(
|
|
||||||
"Some influence tokens already exist in this war.\n"
|
|
||||||
"All tokens will be deleted and tie-breaks will become draw.\n"
|
|
||||||
"Do you want to continue?",
|
|
||||||
action=cleanup_token_and_tie,
|
|
||||||
)
|
|
||||||
if new_state is True:
|
|
||||||
has_tie_resolved = any(isinstance(ev, TieResolved) for ev in self.events)
|
|
||||||
grant_influence = any(
|
|
||||||
(sect.influence_objective_id is not None)
|
|
||||||
for camp in self.campaigns
|
|
||||||
for sect in camp.get_all_sectors()
|
|
||||||
)
|
|
||||||
if not has_tie_resolved and not grant_influence:
|
|
||||||
self.influence_token = new_state
|
|
||||||
return
|
|
||||||
raise RequiresConfirmation(
|
|
||||||
"Some influence tokens and draws exist in this war.\n"
|
|
||||||
"All influence outcomes and tie-breaks will be reset.\n"
|
|
||||||
"Do you want to continue?",
|
|
||||||
action=reset_tie_break,
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_state(self, new_state: bool) -> None:
|
def set_state(self, new_state: bool) -> None:
|
||||||
self.is_over = new_state
|
self.is_over = new_state
|
||||||
|
|
|
||||||
|
|
@ -63,32 +63,19 @@ class WarEvent:
|
||||||
class TieResolved(WarEvent):
|
class TieResolved(WarEvent):
|
||||||
TYPE = "TieResolved"
|
TYPE = "TieResolved"
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, participant_id: str | None, context_type: str, context_id: str):
|
||||||
self,
|
|
||||||
participant_id: str | None,
|
|
||||||
context_type: str,
|
|
||||||
context_id: str,
|
|
||||||
score_value: int | None = None,
|
|
||||||
):
|
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(participant_id, context_type, context_id)
|
||||||
self.score_value = score_value
|
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
d.update(
|
|
||||||
{
|
|
||||||
"score_value": self.score_value or None,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@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,
|
data["participant_id"],
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
data["context_id"],
|
data["context_id"],
|
||||||
data["score_value"] or None,
|
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,6 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
)
|
)
|
||||||
self.majorValue.valueChanged.connect(self._on_major_changed)
|
self.majorValue.valueChanged.connect(self._on_major_changed)
|
||||||
self.minorValue.valueChanged.connect(self._on_minor_changed)
|
self.minorValue.valueChanged.connect(self._on_minor_changed)
|
||||||
self.warsTree.currentItemChanged.connect(self._emit_selection_changed)
|
|
||||||
self._apply_icons()
|
self._apply_icons()
|
||||||
|
|
||||||
def _apply_icons(self) -> None:
|
def _apply_icons(self) -> None:
|
||||||
|
|
@ -144,11 +143,6 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
return "wars"
|
return "wars"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def clear_tree_selection(self) -> None:
|
|
||||||
self.warsTree.blockSignals(True)
|
|
||||||
self.warsTree.setCurrentItem(None)
|
|
||||||
self.warsTree.blockSignals(False)
|
|
||||||
|
|
||||||
# General popups
|
# General popups
|
||||||
|
|
||||||
def closeEvent(self, event: QCloseEvent | None = None) -> None:
|
def closeEvent(self, event: QCloseEvent | None = None) -> None:
|
||||||
|
|
@ -243,7 +237,10 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
|
|
||||||
def display_wars_tree(self, wars: List[WarDTO]) -> None:
|
def display_wars_tree(self, wars: List[WarDTO]) -> None:
|
||||||
tree = self.warsTree
|
tree = self.warsTree
|
||||||
tree.blockSignals(True)
|
try:
|
||||||
|
tree.currentItemChanged.disconnect()
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
tree.clear()
|
tree.clear()
|
||||||
tree.setColumnCount(1)
|
tree.setColumnCount(1)
|
||||||
tree.setHeaderLabels(["Wars"])
|
tree.setHeaderLabels(["Wars"])
|
||||||
|
|
@ -267,8 +264,8 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
rnd_item.setData(0, ROLE_TYPE, ItemType.ROUND)
|
rnd_item.setData(0, ROLE_TYPE, ItemType.ROUND)
|
||||||
rnd_item.setData(0, ROLE_ID, rnd.id)
|
rnd_item.setData(0, ROLE_ID, rnd.id)
|
||||||
camp_item.addChild(rnd_item)
|
camp_item.addChild(rnd_item)
|
||||||
|
tree.currentItemChanged.connect(self._emit_selection_changed)
|
||||||
tree.expandAll()
|
tree.expandAll()
|
||||||
tree.blockSignals(False)
|
|
||||||
|
|
||||||
def select_tree_item(self, *, item_type: ItemType, item_id: str) -> None:
|
def select_tree_item(self, *, item_type: ItemType, item_id: str) -> None:
|
||||||
def walk(item: QTreeWidgetItem) -> bool:
|
def walk(item: QTreeWidgetItem) -> bool:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue