add campaign & round

This commit is contained in:
Maxime Réaux 2026-01-21 07:43:04 +01:00
parent 9e966baf9b
commit 1218f32752
10 changed files with 365 additions and 24 deletions

View file

@ -1,20 +1,29 @@
from pathlib import Path
from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.model.model import Model
from warchron.view.view import View
from warchron.view.view import PlayerDialog, WarDialog
from warchron.view.view import PlayerDialog, WarDialog, CampaignDialog
class Controller:
def __init__(self, model, view):
def __init__(self, model: Model, view: View):
self.model = model
self.view = view
self.current_file: Path | None = None
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
self.view.on_close_callback = self.on_app_close
self.is_dirty = False
self.__connect()
self.refresh_players_view()
self.refresh_wars_view()
self.update_window_title()
self.update_actions_state()
self.view.on_tree_selection_changed = self.on_tree_selection_changed
self.view.on_add_campaign = self.add_campaign
self.view.on_add_round = self.add_round
def __connect(self):
self.view.actionExit.triggered.connect(self.view.close)
@ -107,6 +116,27 @@ class Controller:
players = self.model.get_all_players()
self.view.display_players(players)
def refresh_wars_view(self):
wars = self.model.get_all_wars()
self.view.display_wars(wars)
def on_tree_selection_changed(self, selection):
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
if selection:
if selection["type"] == "war":
self.selected_war_id = selection["id"]
elif selection["type"] == "campaign":
self.selected_campaign_id = selection["id"]
elif selection["type"] == "round":
self.selected_round_id = selection["id"]
self.update_actions_state()
def update_actions_state(self):
self.view.set_add_campaign_enabled(self.selected_war_id is not None)
self.view.set_add_round_enabled(self.selected_campaign_id is not None)
def add_player(self):
dialog = PlayerDialog(self.view)
result = dialog.exec() # modal blocking dialog
@ -123,11 +153,7 @@ class Controller:
self.is_dirty = True
self.refresh_players_view()
self.update_window_title()
def refresh_wars_view(self):
wars = self.model.get_all_wars()
self.view.display_wars(wars)
def add_war(self):
dialog = WarDialog(self.view)
result = dialog.exec() # modal blocking dialog
@ -144,3 +170,25 @@ class Controller:
self.is_dirty = True
self.refresh_wars_view()
self.update_window_title()
def add_campaign(self):
if not self.selected_war_id:
return
dialog = CampaignDialog(self.view)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
name = dialog.get_campaign_name()
if not name:
return
self.model.add_campaign(self.selected_war_id, name)
self.is_dirty = True
self.refresh_wars_view()
self.update_window_title()
def add_round(self):
if not self.selected_campaign_id:
return
self.model.add_round(self.selected_campaign_id)
self.is_dirty = True
self.refresh_wars_view()
self.update_window_title()

View file

@ -9,7 +9,7 @@ class Campaign:
self.name = name
self.month = datetime.now().month
self.entrants = {}
self.rounds = {}
self.rounds = []
self.is_over = False
def set_id(self, new_id):
@ -42,4 +42,20 @@ class Campaign:
## entrants placeholder
## rounds placeholder
tmp.set_state(is_over)
return tmp
return tmp
def add_round(self, number: int) -> Round:
round = Round()
self.rounds.append(round)
return round
def get_round(self, round_id) -> Round:
return self.rounds[round_id]
def get_all_rounds(self) -> list[Round]:
return list(self.rounds)
def add_round(self) -> Round:
round = Round()
self.rounds.append(round)
return round

View file

@ -4,6 +4,8 @@ import shutil
from warchron.model.player import Player
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.round import Round
class Model:
def __init__(self):
@ -69,13 +71,36 @@ class Model:
def get_all_players(self) -> list[Player]:
return list(self.players.values())
def add_war(self, name):
def add_war(self, name) -> War:
war = War(name)
self.wars[war.id] = war
return war
def get_war(self, id):
def get_war(self, id) -> War:
return self.wars[id]
def get_all_wars(self) -> list[War]:
return list(self.wars.values())
def add_campaign(self, war_id: str, name: str) -> Campaign:
war = self.get_war(war_id)
return war.add_campaign(name)
def get_campaign(self, campaign_id) -> Campaign:
for war in self.wars.values():
for campaign in war.campaigns:
if campaign.id == campaign_id:
return campaign
raise KeyError("Campaign not found")
def add_round(self, campaign_id: str) -> Round:
campaign = self.get_campaign(campaign_id)
return campaign.add_round()
def get_round(self, round_id: str) -> Round:
for war in self.wars.values():
for campaign in war.campaigns:
for rnd in campaign.rounds:
if rnd.id == round_id:
return rnd
raise KeyError("Round not found")

View file

@ -1,20 +1,21 @@
from uuid import uuid4
class Round:
def __init__(self, number):
self.number = number
def __init__(self):
self.id = str(uuid4())
self.sectors = {}
self.choices = {}
self.battles = {}
self.is_over = False
def set_number(self, new_number):
self.number = new_number
def set_id(self, new_id):
self.id = new_id
def set_state(self, new_state):
self.is_over = new_state
def toDict(self):
return {
"number" : self.number,
"sectors" : self.sectors,
"choices" : self.choices,
"battles" : self.battles,
@ -22,8 +23,9 @@ class Round:
}
@staticmethod
def fromDict(id, number, sectors, choices, battles, is_over):
tmp = Round(number=number)
def fromDict(id, sectors, choices, battles, is_over):
tmp = Round()
tmp.set_id(id)
## sectors placeholder
## choices placeholder
## battles placeholder

View file

@ -9,10 +9,9 @@ class War:
self.name = name
self.year = datetime.now().year
self.entrants = {}
self.campaigns = {}
self.campaigns = []
self.is_over = False
def set_id(self, new_id):
self.id = new_id
@ -43,4 +42,20 @@ class War:
## entrants placeholder
## campaigns placeholder
tmp.set_state(is_over)
return tmp
return tmp
def add_campaign(self, name) -> Campaign:
campaign = Campaign(name)
self.campaigns.append(campaign)
return campaign
def get_campaign(self, campaign_id) -> Campaign:
return self.campaigns[campaign_id]
def get_all_campaigns(self) -> list[Campaign]:
return list(self.campaigns)
def add_campaign(self, name: str) -> Campaign:
campaign = Campaign(name)
self.campaigns.append(campaign)
return campaign

View file

@ -0,0 +1,50 @@
# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_campaign_dialog.ui'
#
# Created by: PyQt6 UI code generator 6.7.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_campaignDialog(object):
def setupUi(self, campaignDialog):
campaignDialog.setObjectName("campaignDialog")
campaignDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
campaignDialog.resize(378, 98)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
campaignDialog.setWindowIcon(icon)
self.buttonBox = QtWidgets.QDialogButtonBox(parent=campaignDialog)
self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32))
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setObjectName("buttonBox")
self.label = QtWidgets.QLabel(parent=campaignDialog)
self.label.setGeometry(QtCore.QRect(10, 20, 47, 14))
self.label.setObjectName("label")
self.campaignName = QtWidgets.QLineEdit(parent=campaignDialog)
self.campaignName.setGeometry(QtCore.QRect(60, 20, 113, 20))
self.campaignName.setObjectName("campaignName")
self.retranslateUi(campaignDialog)
self.buttonBox.accepted.connect(campaignDialog.accept) # type: ignore
self.buttonBox.rejected.connect(campaignDialog.reject) # type: ignore
QtCore.QMetaObject.connectSlotsByName(campaignDialog)
def retranslateUi(self, campaignDialog):
_translate = QtCore.QCoreApplication.translate
campaignDialog.setWindowTitle(_translate("campaignDialog", "Campaign"))
self.label.setText(_translate("campaignDialog", "Name:"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
campaignDialog = QtWidgets.QDialog()
ui = Ui_campaignDialog()
ui.setupUi(campaignDialog)
campaignDialog.show()
sys.exit(app.exec())

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>campaignDialog</class>
<widget class="QDialog" name="campaignDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>378</width>
<height>98</height>
</rect>
</property>
<property name="windowTitle">
<string>Campaign</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>../resources/warchron_logo.png</normaloff>../resources/warchron_logo.png</iconset>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>10</x>
<y>60</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>47</width>
<height>14</height>
</rect>
</property>
<property name="text">
<string>Name:</string>
</property>
</widget>
<widget class="QLineEdit" name="campaignName">
<property name="geometry">
<rect>
<x>60</x>
<y>20</y>
<width>113</width>
<height>20</height>
</rect>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>campaignDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>campaignDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -51,12 +51,16 @@ class Ui_MainWindow(object):
self.addCampaignBtn.setEnabled(False)
self.addCampaignBtn.setGeometry(QtCore.QRect(110, 20, 91, 23))
self.addCampaignBtn.setObjectName("addCampaignBtn")
self.addRoundBtn = QtWidgets.QPushButton(parent=self.warsTab)
self.addRoundBtn.setEnabled(False)
self.addRoundBtn.setGeometry(QtCore.QRect(220, 20, 91, 23))
self.addRoundBtn.setObjectName("addRoundBtn")
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/swords-small.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.tabWidget.addTab(self.warsTab, icon2, "")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22))
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(parent=self.menubar)
self.menuFile.setObjectName("menuFile")
@ -144,6 +148,7 @@ class Ui_MainWindow(object):
self.tabWidget.setTabText(self.tabWidget.indexOf(self.playersTab), _translate("MainWindow", "Players"))
self.addWarBtn.setText(_translate("MainWindow", "Add war"))
self.addCampaignBtn.setText(_translate("MainWindow", "Add Campaign"))
self.addRoundBtn.setText(_translate("MainWindow", "Add Round"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.warsTab), _translate("MainWindow", "Wars"))
self.menuFile.setTitle(_translate("MainWindow", "File"))
self.menuEdit.setTitle(_translate("MainWindow", "Edit"))

View file

@ -124,6 +124,22 @@
<string>Add Campaign</string>
</property>
</widget>
<widget class="QPushButton" name="addRoundBtn">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>220</x>
<y>20</y>
<width>91</width>
<height>23</height>
</rect>
</property>
<property name="text">
<string>Add Round</string>
</property>
</widget>
</widget>
</widget>
</widget>
@ -133,7 +149,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>22</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">

View file

@ -1,18 +1,49 @@
from pathlib import Path
import calendar
from PyQt6 import QtWidgets
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QDialog, QFileDialog, QTreeWidgetItem
from PyQt6.QtGui import QCloseEvent
from warchron.view.ui.ui_main_window import Ui_MainWindow
from warchron.view.ui.ui_player_dialog import Ui_playerDialog
from warchron.view.ui.ui_war_dialog import Ui_warDialog
from warchron.view.ui.ui_campaign_dialog import Ui_campaignDialog
ROLE_TYPE = Qt.ItemDataRole.UserRole
ROLE_ID = Qt.ItemDataRole.UserRole + 1
class View(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(View, self).__init__(parent)
self.setupUi(self)
self.on_close_callback = None
self.on_selection_changed = None
self.on_add_campaign = None
self.on_add_round = None
self.warsTree.currentItemChanged.connect(self._emit_selection_changed)
self.addCampaignBtn.clicked.connect(self._on_add_campaign_clicked)
self.addRoundBtn.clicked.connect(self._on_add_round_clicked)
def _emit_selection_changed(self, current, previous):
if not self.on_tree_selection_changed:
return
if not current:
self.on_tree_selection_changed(None)
return
self.on_tree_selection_changed({
"type": current.data(0, ROLE_TYPE),
"id": current.data(0, ROLE_ID),
})
def _on_add_campaign_clicked(self):
if self.on_add_campaign:
self.on_add_campaign()
def _on_add_round_clicked(self):
if self.on_add_round:
self.on_add_round()
def closeEvent(self, event: QCloseEvent):
if self.on_close_callback:
@ -55,10 +86,36 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
tree.setHeaderLabels(["Wars"])
for war in wars:
war_item = QTreeWidgetItem([f"{war.name} ({war.year})"])
war_item.setData(0, 1, war.id) # role=1 to store id
war_item.setData(0, ROLE_TYPE, "war")
war_item.setData(0, ROLE_ID, war.id)
tree.addTopLevelItem(war_item)
for camp in war.get_all_campaigns():
camp_item = QTreeWidgetItem([f"{camp.name} ({calendar.month_name[camp.month]})"])
camp_item.setData(0, ROLE_TYPE, "campaign")
camp_item.setData(0, ROLE_ID, camp.id)
war_item.addChild(camp_item)
for index, rnd in enumerate(camp.get_all_rounds(), start=1):
rnd_item = QTreeWidgetItem([f"Round {index}"])
rnd_item.setData(0, ROLE_TYPE, "round")
rnd_item.setData(0, ROLE_ID, rnd.id)
camp_item.addChild(rnd_item)
tree.expandAll()
def get_selected_tree_item(self):
item = self.warsTree.currentItem()
if not item:
return None
return {
"type": item.data(0, ROLE_TYPE),
"id": item.data(0, ROLE_ID)
}
def set_add_campaign_enabled(self, enabled: bool):
self.addCampaignBtn.setEnabled(enabled)
def set_add_round_enabled(self, enabled: bool):
self.addRoundBtn.setEnabled(enabled)
class PlayerDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
@ -76,3 +133,12 @@ class WarDialog(QDialog):
def get_war_name(self) -> str:
return self.ui.warName.text().strip()
class CampaignDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_campaignDialog()
self.ui.setupUi(self)
def get_campaign_name(self) -> str:
return self.ui.campaignName.text().strip()