diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d2f93c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +ress: + pyrcc5 .\src\wargame_campaign\view\resources\ui_ressources.qrc -o .\src\wargame_campaign\view\resources\ui_ressources_rc.py + +installer : + python -m PyInstaller .\main.spec + +ui: +UI_DIR := ./view/ui +UI_FILES := $(wildcard $(UI_DIR)/*.ui) +PY_FILES := $(UI_FILES:.ui=.py) + +# Generate all .py UI modules from .ui files +ui: $(PY_FILES) + +# Pattern rule: .ui -> .py using pyuic5 +$(UI_DIR)/%.py: $(UI_DIR)/%.ui + pyuic5 -x $< -o $@ --import-from wargame_campaign.view.resources + +# Function to generate UI file from given name +_ui_generate: + pyuic6 -x .\src\wargame_campaign\view\ui\$(UI_NAME).ui -o .\src\wargame_campaign\view\ui\$(UI_NAME).py --import-from wargame_campaign.view.resources + +# Set default UI_NAME if not provided +UI_NAME ?= ui_main_window diff --git a/README.md b/README.md index 1982b3c..ec3c5cd 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,39 @@ # Wargame_campaign_app -A simple CLI app to manage players and their scores throughout several organised games of a tabletop wargame. +A simple local app to manage players and their scores throughout several organised games of a tabletop wargame. -## Main logic +## Features + +### Main logic Manage a list of players to sign them up to be selectable for war(s) and campaign(s). A year "war" contains several "campaign" events which contain several "battle" games organised in successive rounds. Battle results determine campaign score which determines the war score. Wars are independent. -## Design notes +### Design notes Players are global identities Influence tokens are scoped to a war Campaign order enables historical tie-breakers -Effects are generic → future-proof \ No newline at end of file +Effects are generic → future-proof + +## Installation + +### Requirements + +- Python >= 3.12 +- pip + +### Setup + +```bash +git clone /Wargame_campaign_app.git +cd Wargame_campaign_app +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt +``` + +### Run + +`python main.py` diff --git a/cli/app.py b/cli/app.py deleted file mode 100644 index 2064696..0000000 --- a/cli/app.py +++ /dev/null @@ -1,38 +0,0 @@ -from cli.war_menu import war_menu -from cli.utils import choose_from_list - -def app_menu(data): - while True: - print("\n=== Warmachron ===") - print("1. Select war") - print("2. Create war") - print("3. Manage players") - print("0. Exit") - - choice = input("> ").strip() - - if choice == "1": - war = select_war(data) - if war: - war_menu(data, war) - - elif choice == "2": - create_war(data) - - elif choice == "3": - manage_players(data) - - elif choice == "0": - return - -def select_war(data): - wars = data["wars"] - if not wars: - print("No wars available.") - return None - - return choose_from_list( - wars, - lambda w: f"{w['name']} ({w['year']})" - ) - diff --git a/cli/campaign_menu.py b/cli/campaign_menu.py deleted file mode 100644 index c29239e..0000000 --- a/cli/campaign_menu.py +++ /dev/null @@ -1,23 +0,0 @@ -from cli.round_menu import round_menu - -def campaign_menu(data, war, campaign): - while True: - print(f"\n=== Campaign: {campaign['name']} ===") - print("1. Select round") - print("2. Append round") - print("3. Finish campaign") - print("4. Edit/Delete campaign") - print("0. Back") - - choice = input("> ").strip() - - if choice == "1": - rnd = select_round(campaign) - if rnd: - round_menu(data, war, campaign, rnd) - - elif choice == "2": - append_round(campaign) - - elif choice == "0": - return diff --git a/cli/round_menu.py b/cli/round_menu.py deleted file mode 100644 index 6c98c17..0000000 --- a/cli/round_menu.py +++ /dev/null @@ -1,34 +0,0 @@ -def round_menu(data, war, campaign, rnd): - while True: - print(f"\n=== Round {rnd['number']} ===") - print("1. Enter choices and pairing") - print("2. Enter battle results") - print("3. Finish round") - print("4. Edit/Delete round") - print("0. Back") - - choice = input("> ").strip() - - if choice == "1": - enter_choices(data, war, campaign, rnd) - - elif choice == "2": - enter_battle_results(data, war, campaign, rnd) - - elif choice == "0": - return - -def enter_choices(data, war, campaign, rnd): - print("Entering choices (placeholder)") - # later: - # - list campaign participants - # - input primary / secondary sector - # - store in rnd["choices"] - -def enter_battle_results(data, war, campaign, rnd): - print("Entering battle results (placeholder)") - # later: - # - list battles - # - select winner - # - apply scoring - diff --git a/cli/utils.py b/cli/utils.py deleted file mode 100644 index 32045e7..0000000 --- a/cli/utils.py +++ /dev/null @@ -1,14 +0,0 @@ -def choose_from_list(items, label_fn): - for i, item in enumerate(items, start=1): - print(f"{i}. {label_fn(item)}") - print("0. Back") - - choice = input("> ").strip() - if choice == "0": - return None - - try: - return items[int(choice) - 1] - except (ValueError, IndexError): - print("Invalid choice") - return None diff --git a/cli/war_menu.py b/cli/war_menu.py deleted file mode 100644 index 8400888..0000000 --- a/cli/war_menu.py +++ /dev/null @@ -1,33 +0,0 @@ -from cli.campaign_menu import campaign_menu -from storage.repository import save_data - -def war_menu(data, war): - while True: - print(f"\n=== War: {war['name']} ===") - print("1. Select campaign") - print("2. Append campaign") - print("3. Finish war") - print("4. Edit/Delete war") - print("0. Back") - - choice = input("> ").strip() - - if choice == "1": - campaign = select_campaign(war) - if campaign: - campaign_menu(data, war, campaign) - - elif choice == "2": - append_campaign(war) - - elif choice == "0": - return - -def append_campaign(war, data): - name = input("Campaign name: ") - war["campaigns"].append({ - "name": name, - "rounds": [], - "completed": False - }) - save_data(data) diff --git a/data/example.json b/data/example.json index bad78d4..10ae5b8 100644 --- a/data/example.json +++ b/data/example.json @@ -1,17 +1,20 @@ { "version": 1, - "players": { - "P1": { - "name": "Alice" + "players": [ + { + "id" : "p1", + "name" :"Alice" }, - "P2": { - "name": "Bob" + { + "id" : "p2", + "name" :"Bob" }, - "P3": { - "name": "Charlie" + { + "id" : "p3", + "name" :"Charlie" } - }, + ], "wars": [ { @@ -20,11 +23,11 @@ "year": 2025, "registered_players": { - "P1": { + "p1": { "war_points": 0, "influence_tokens": 1 }, - "P2": { + "p2": { "war_points": 0, "influence_tokens": 0 } @@ -35,10 +38,10 @@ "id": "CAMP01", "name": "Widower's Wood", "order": 1, - + "month" : "June", "participants": { - "P1": { "campaign_points": 0 }, - "P2": { "campaign_points": 0 } + "p1": { "campaign_points": 0 }, + "p2": { "campaign_points": 0 } }, "rounds": [ @@ -47,15 +50,15 @@ "sectors": ["North", "South"], "choices": { - "P1": { "primary": "North", "secondary": "South" }, - "P2": { "primary": "North", "secondary": "South" } + "p1": { "primary": "North", "secondary": "South" }, + "p2": { "primary": "North", "secondary": "South" } }, "battles": [ { "sector": "North", - "players": ["P1", "P2"], - "winner": "P1", + "players": ["p1", "p2"], + "winner": "p1", "effects": { "campaign_points": 1, diff --git a/main.py b/main.py index 45047d5..ced14b9 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,23 @@ -from storage.repository import load_data, save_data -from cli.app import app_menu +import sys +import os +sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "src")) -def main(): - data = load_data() - app_menu(data) - save_data(data) +from PyQt6.QtWidgets import QApplication + +from wargame_campaign.view.view import View +from wargame_campaign.model.model import Model +from wargame_campaign.controller.controller import Controller + +if sys.version_info < (3, 12): + raise RuntimeError("Python 3.12 or higher is required") if __name__ == "__main__": - main() + app = QApplication(sys.argv) + + view = View() + model = Model() + controller = Controller(model, view) + + view.show() + + sys.exit(app.exec()) diff --git a/models/player.py b/models/player.py deleted file mode 100644 index 79a1c99..0000000 --- a/models/player.py +++ /dev/null @@ -1,4 +0,0 @@ -def create_player(player_id, name): - return { - "name": name - } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..abcc883 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyQt6>=6.6,<6.8 \ No newline at end of file diff --git a/services/round_service.py b/services/round_service.py deleted file mode 100644 index f04f796..0000000 --- a/services/round_service.py +++ /dev/null @@ -1,7 +0,0 @@ -def resolve_round(round_data, campaign, war): - """ - Placeholder: - - resolve sector assignments - - create battle entries - """ - pass diff --git a/services/scoring_service.py b/services/scoring_service.py deleted file mode 100644 index b7c36ba..0000000 --- a/services/scoring_service.py +++ /dev/null @@ -1,10 +0,0 @@ -def apply_battle_effects(battle, campaign, war): - """ - Placeholder scoring logic. - """ - winner = battle["winner"] - effects = battle.get("effects", {}) - - campaign_points = effects.get("campaign_points", 0) - if winner in campaign["participants"]: - campaign["participants"][winner]["campaign_points"] += campaign_points diff --git a/services/tie_breaker_service.py b/services/tie_breaker_service.py deleted file mode 100644 index 09fb37f..0000000 --- a/services/tie_breaker_service.py +++ /dev/null @@ -1,7 +0,0 @@ -def break_tie(players, current_campaign, war): - """ - Placeholder: - - future implementation will check - previous campaigns and influence tokens - """ - return players diff --git a/src/wargame_campaign/controller/__ini__.py b/src/wargame_campaign/controller/__ini__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wargame_campaign/controller/controller.py b/src/wargame_campaign/controller/controller.py new file mode 100644 index 0000000..5f77d49 --- /dev/null +++ b/src/wargame_campaign/controller/controller.py @@ -0,0 +1,15 @@ +class Controller: + def __init__(self, model, view): + self.model = model + self.view = view + self.__connect() + + + def __connect(self): + # self.view.players_view.btn_add.clicked.connect(self.add_player) + pass + + def add_player(self): + print(f"test") + + \ No newline at end of file diff --git a/src/wargame_campaign/model/__ini__.py b/src/wargame_campaign/model/__ini__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wargame_campaign/model/model.py b/src/wargame_campaign/model/model.py new file mode 100644 index 0000000..2065b9e --- /dev/null +++ b/src/wargame_campaign/model/model.py @@ -0,0 +1,55 @@ +from pathlib import Path +import json +import shutil + +from wargame_campaign.model.player import Player + +class Model: + def __init__(self): + self.players = {} + data_file_path = Path("data/warmachron.json") + self.load_data(data_file_path) + + def load_data(self, data_file_path): + if not data_file_path.exists() or data_file_path.stat().st_size == 0: + pass # Create empty json + try: + with open(data_file_path, "r", encoding="utf-8") as f: + data = json.load(f) + for player in data["players"] : + print(f"player {player}") + saved_player = Player.fromDict(player["id"], player['name']) + self.players[saved_player.id] = saved_player + for war in data["wars"]: + pass + except json.JSONDecodeError: + raise RuntimeError("Data file is corrupted") + + def save_data(self, data_file_path): + if data_file_path.exists(): + shutil.copy(data_file_path, data_file_path.with_suffix(".json.bak")) + + data = {} + data['verion'] = "1.0" + data['players'] = [] + for player in self.players: + data['players'].append(player.toDict()) + + with open(data_file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + def add_player(self, name): + player = Player(name) + self.players[player.id] = player + return player + + def get_player(self, id): + return self.players[id] + + def update_player(self, id, name): + player = self.get_player(id) + player.set_name(name) + + def delete_player(self, id): + del self.players[id] + diff --git a/src/wargame_campaign/model/player.py b/src/wargame_campaign/model/player.py new file mode 100644 index 0000000..a835fa1 --- /dev/null +++ b/src/wargame_campaign/model/player.py @@ -0,0 +1,24 @@ +from uuid import uuid4 + +class Player: + def __init__(self, name ): + self.id = str(uuid4()) + self.name = name + + def set_id(self, new_id): + self.id = new_id + + def set_name(self, name): + self.name = name + + def toDict(self): + return { + "id" : self.id, + "name" : self.name + } + + @staticmethod + def fromDict(id, name): + tmp = Player(name=name) + tmp.set_id(id) + return tmp \ No newline at end of file diff --git a/src/wargame_campaign/model/player_service.py b/src/wargame_campaign/model/player_service.py new file mode 100644 index 0000000..c81f52e --- /dev/null +++ b/src/wargame_campaign/model/player_service.py @@ -0,0 +1,23 @@ +from models.player import create_player +from model.repository import save_data + +def generate_player_id(players): + return f"P{len(players) + 1}" + +def add_player(data, name): + players = data["players"] + + if any(p["name"].lower() == name.lower() for p in players.values()): + raise ValueError("Player already exists") + + player_id = generate_player_id(players) + players[player_id] = create_player(player_id, name) + save_data(data) + +def update_player(data, player_id, new_name): + data["players"][player_id]["name"] = new_name + save_data(data) + +def delete_player(data, player_id): + del data["players"][player_id] + save_data(data) \ No newline at end of file diff --git a/storage/repository.py b/src/wargame_campaign/model/repository.py similarity index 100% rename from storage/repository.py rename to src/wargame_campaign/model/repository.py diff --git a/src/wargame_campaign/view/__ini__.py b/src/wargame_campaign/view/__ini__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wargame_campaign/view/ui/ui_main_window.py b/src/wargame_campaign/view/ui/ui_main_window.py new file mode 100644 index 0000000..f72092b --- /dev/null +++ b/src/wargame_campaign/view/ui/ui_main_window.py @@ -0,0 +1,42 @@ +# Form implementation generated from reading ui file '.\src\wargame_campaign\view\ui\ui_main_window.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_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(800, 600) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) + self.centralwidget.setObjectName("centralwidget") + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(parent=MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21)) + self.menubar.setObjectName("menubar") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + MainWindow = QtWidgets.QMainWindow() + ui = Ui_MainWindow() + ui.setupUi(MainWindow) + MainWindow.show() + sys.exit(app.exec()) diff --git a/src/wargame_campaign/view/ui/ui_main_window.ui b/src/wargame_campaign/view/ui/ui_main_window.ui new file mode 100644 index 0000000..7bff7e1 --- /dev/null +++ b/src/wargame_campaign/view/ui/ui_main_window.ui @@ -0,0 +1,31 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 0 + 0 + 800 + 21 + + + + + + + + diff --git a/src/wargame_campaign/view/view.py b/src/wargame_campaign/view/view.py new file mode 100644 index 0000000..cf3c3ac --- /dev/null +++ b/src/wargame_campaign/view/view.py @@ -0,0 +1,9 @@ +from PyQt6 import uic, QtWidgets +from wargame_campaign.view.ui.ui_main_window import Ui_MainWindow + +class View(QtWidgets.QMainWindow, Ui_MainWindow): + def __init__(self, parent=None): + super(View, self).__init__(parent) + self.setupUi(self) + +