diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cda3536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# ========================= +# Python bytecode & cache +# ========================= +__pycache__/ +*.py[cod] +*$py.class + +# ========================= +# Virtual environments +# ========================= +.env +.venv +env/ +venv/ +ENV/ + +# ========================= +# Packaging / build +# ========================= +build/ +dist/ +*.egg-info/ +.eggs/ + +# ========================= +# Test & coverage +# ========================= +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ + +# ========================= +# IDE / editors +# ========================= +.vscode/ +.idea/ +*.swp +*.swo + +# ========================= +# OS generated files +# ========================= +.DS_Store +Thumbs.db + +# ========================= +# Logs +# ========================= +*.log + +# ========================= +# Application data +# ========================= +data/*.json +data/*.bak + +# But keep example files +!data/example.json diff --git a/README.md b/README.md index 26903dd..1982b3c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# Wargame_campain_app +# Wargame_campaign_app -A simplie CLI app to manage players and their scores throughout several organised games of a tabletop wargame. \ No newline at end of file +A simple CLI app to manage players and their scores throughout several organised games of a tabletop wargame. + +## 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 + +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 diff --git a/cli/menu.py b/cli/menu.py new file mode 100644 index 0000000..91f46ce --- /dev/null +++ b/cli/menu.py @@ -0,0 +1,9 @@ +def main_menu(): + print("\n=== Wargame Campaign App ===") + print("1. List wars") + print("2. Start new campaign") + print("3. Start new round") + print("4. Enter battle results") + print("5. Save & exit") + + return input("> ") diff --git a/data/example.json b/data/example.json new file mode 100644 index 0000000..bad78d4 --- /dev/null +++ b/data/example.json @@ -0,0 +1,78 @@ +{ + "version": 1, + + "players": { + "P1": { + "name": "Alice" + }, + "P2": { + "name": "Bob" + }, + "P3": { + "name": "Charlie" + } + }, + + "wars": [ + { + "id": "WAR2025", + "name": "Llael War 2025", + "year": 2025, + + "registered_players": { + "P1": { + "war_points": 0, + "influence_tokens": 1 + }, + "P2": { + "war_points": 0, + "influence_tokens": 0 + } + }, + + "campaigns": [ + { + "id": "CAMP01", + "name": "Widower's Wood", + "order": 1, + + "participants": { + "P1": { "campaign_points": 0 }, + "P2": { "campaign_points": 0 } + }, + + "rounds": [ + { + "number": 1, + "sectors": ["North", "South"], + + "choices": { + "P1": { "primary": "North", "secondary": "South" }, + "P2": { "primary": "North", "secondary": "South" } + }, + + "battles": [ + { + "sector": "North", + "players": ["P1", "P2"], + "winner": "P1", + + "effects": { + "campaign_points": 1, + "grants_influence_token": false + } + } + ], + + "completed": true + } + ], + + "completed": false + } + ], + + "completed": false + } + ] +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..b0f23a0 --- /dev/null +++ b/main.py @@ -0,0 +1,21 @@ +from storage.repository import load_data, save_data +from cli.menu import main_menu + +def main(): + data = load_data() + + while True: + choice = main_menu() + + if choice == "1": + print("Wars:") + for war in data["wars"]: + print(f"- {war['name']}") + + elif choice == "5": + save_data(data) + print("Goodbye.") + break + +if __name__ == "__main__": + main() diff --git a/models/player.py b/models/player.py new file mode 100644 index 0000000..79a1c99 --- /dev/null +++ b/models/player.py @@ -0,0 +1,4 @@ +def create_player(player_id, name): + return { + "name": name + } diff --git a/services/round_service.py b/services/round_service.py new file mode 100644 index 0000000..f04f796 --- /dev/null +++ b/services/round_service.py @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..b7c36ba --- /dev/null +++ b/services/scoring_service.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..09fb37f --- /dev/null +++ b/services/tie_breaker_service.py @@ -0,0 +1,7 @@ +def break_tie(players, current_campaign, war): + """ + Placeholder: + - future implementation will check + previous campaigns and influence tokens + """ + return players diff --git a/storage/repository.py b/storage/repository.py new file mode 100644 index 0000000..4122482 --- /dev/null +++ b/storage/repository.py @@ -0,0 +1,22 @@ +import json +import shutil +from pathlib import Path + +DATA_FILE = Path("data/warmachron.json") + +def load_data(): + if not DATA_FILE.exists() or DATA_FILE.stat().st_size == 0: + return {"version": 1, "players": {}, "wars": []} + + try: + with open(DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError: + raise RuntimeError("Data file is corrupted") + +def save_data(data): + if DATA_FILE.exists(): + shutil.copy(DATA_FILE, DATA_FILE.with_suffix(".json.bak")) + + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2)