diff --git a/.gitignore b/.gitignore index ad1ef2b..e61085c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .venv/ .python-version __pycache__/ -uv.lock +*.lock # Save/Load files for testing *.json \ No newline at end of file diff --git a/README.md b/README.md index 3eec51e..e5b554d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Wyvern&Castle -Projet de NLP 2025-2026. Modèle MCP de D&D. +Ceci est un projet de **modèle MCP** ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) pour le cours de **Natural Language Programming** de l'année 2025-2026. + +L'objectif du projet a été de concevoir une **version simplifié du jeu Donjon & Dragon** avec un LLM capable de générer des parties et des scénarios qui n'ont de limite que votre imagination (et celles du LLM aussi!). +Le projet contient un serveur (`server.py`) qui intéragit avec notre API de jeu (`game.py`). -# Initialisation du jeu +## Initialisation du projet +Pour lancer une partie de notre jeu, nous vous conseillons d'installer **[Claude Desktop](https://claude.com/fr-fr/download)** (disponible sur Windows +et Mac) qui va servir de LLM. Nous n'avons pas essayé mais il serait aussi possible d'utiliser la **version Desktop de ChatGPT** -Pour lancer la partie il faut tout d'abord installer [Claude Desktop](https://claude.com/fr-fr/download) (disponible sur Windows -et Mac). - -## Installer l'utilitaire python UV : +1. Installer l'utilitaire python UV : ```bash # Mac/Linux curl -LsSf https://astral.sh/uv/install.sh | sh @@ -15,14 +17,14 @@ curl -LsSf https://astral.sh/uv/install.sh | sh powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -## Initialiser le dossier pour l'installation +2. Initialiser le dossier pour l'installation Creer un dossier et cloner le projet : ```bash -uv init wyvern-castle -cd wyvern-castle +mkdir wyvern_castle +cd wyvern_castle git clone "https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle.git" ``` -## Creation de l'environnement virtuel +3. Creation de l'environnement virtuel ```bash uv venv ``` @@ -35,18 +37,24 @@ Windows .\.venv\Scripts\activate ``` -## Installation du client mcp +4. Installation des dependances/requirements ```bash -uv add mcp[cli] httpx +# Synchronise l'environnement virtuel du dossier avec les dependances du projet +uv pip sync pyproject.toml + +# Si cela ne fonctionne pas correctement, vous pouvez le générer un fichier de dependances avec la commande suivante à partir du .toml: +uv pip compile --upgrade pyproject.toml -o uv.lock +# Puis synchroniser à nouveau (avec le nouveau fichier cette fois) +uv pip sync uv.lock ``` -## Changement de la config de Claude Desktop +5. Changement de la config de Claude Desktop Modifier le fichier `claude_desktop_config.json` ```json { "mcpServers": { - "weather": { + "wyvern_castle": { "command": "uv", "args": [ "--directory", @@ -59,3 +67,5 @@ Modifier le fichier `claude_desktop_config.json` } ``` +## Utilisation +Le projet est assez simple d'utilisation car une fois le serveur lancé, il vous suffit d'écrire des prompts à l'aide de votre application Desktop de LLM. Il est aussi possible d'avoir accès à une aide de génération de prompt intégrée. diff --git a/entities/entity.py b/entities/entity.py index 8b8ca44..0ff8eb5 100644 --- a/entities/entity.py +++ b/entities/entity.py @@ -1,95 +1,114 @@ -# Game imports -from serializable import Serializable +# Game imports +from utils.serializable import Serializable +from utils.item import Item # Native imports import uuid class Entity(Serializable): - def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None): + def __init__(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item=None): self.id = str(uuid.uuid4()) self.name = name - self.strength = strength - self.dexterity = dexterity - self.intelligence = intelligence - self.wisdom = wisdom - self.charisma = charisma - self.hp = hp - self.armor = armor - self.speed = speed + self.strength = 20 + self.dexterity = 20 + self.intelligence = 20 + self.wisdom = 20 + self.charisma = 20 + self.hp = 100 + self.armor = 100 + self.speed = 100 + + self.set_strength(strength) + self.set_dexterity(dexterity) + self.set_intelligence(intelligence) + self.set_wisdom(wisdom) + self.set_charisma(charisma) + self.set_hp(hp) + self.set_armor(armor) + self.set_speed(speed) + self.equipped_item = None + if equipped_item: self.set_equipped_item(equipped_item) - + + def clamp(self, valeur, min_val, max_val): + return max(min_val, min(valeur, max_val)) + def get_id(self): return self.id def get_strength(self): if self.equipped_item and "strength" in self.equipped_item.stat_modifier.keys(): - return self.strength + self.equipped_item.stat_modifier["strength"] - return self.strength + return self.clamp(self.strength + self.equipped_item.stat_modifier["strength"], 0, 20) + return self.clamp(self.strength, 0, 20) def get_dexterity(self): if self.equipped_item and "dexterity" in self.equipped_item.stat_modifier.keys(): - return self.dexterity + self.equipped_item.stat_modifier["dexterity"] - return self.dexterity + return self.clamp(self.dexterity + self.equipped_item.stat_modifier["dexterity"], 0, 20) + return self.clamp(self.dexterity, 0, 20) def get_intelligence(self): if self.equipped_item and "intelligence" in self.equipped_item.stat_modifier.keys(): - return self.intelligence + self.equipped_item.stat_modifier["intelligence"] - return self.intelligence + return self.clamp(self.intelligence + self.equipped_item.stat_modifier["intelligence"], 0, 20) + return self.clamp(self.intelligence, 0, 20) def get_wisdom(self): if self.equipped_item and "wisdom" in self.equipped_item.stat_modifier.keys(): - return self.wisdom + self.equipped_item.stat_modifier["wisdom"] - return self.wisdom + return self.clamp(self.wisdom + self.equipped_item.stat_modifier["wisdom"], 0, 20) + return self.clamp(self.wisdom, 0, 20) def get_charisma(self): if self.equipped_item and "charisma" in self.equipped_item.stat_modifier.keys(): - return self.charisma + self.equipped_item.stat_modifier["charisma"] - return self.charisma + return self.clamp(self.charisma + self.equipped_item.stat_modifier["charisma"], 0, 20) + return self.clamp(self.charisma, 0, 20) def get_hp(self): if self.equipped_item and "hp" in self.equipped_item.stat_modifier.keys(): - return self.hp + self.equipped_item.stat_modifier["hp"] - return self.hp + return self.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 100) + return self.clamp(self.hp, 0, 100) def get_armor(self): if self.equipped_item and "armor" in self.equipped_item.stat_modifier.keys(): - return self.armor + self.equipped_item.stat_modifier["armor"] - return self.armor + return self.clamp(self.armor + self.equipped_item.stat_modifier["armor"], 0, 100) + return self.clamp(self.armor, 0, 100) def get_speed(self): if self.equipped_item and "speed" in self.equipped_item.stat_modifier.keys(): - return self.speed + self.equipped_item.stat_modifier["speed"] - return self.speed + return self.clamp(self.speed + self.equipped_item.stat_modifier["speed"], 0, 100) + return self.clamp(self.speed, 0, 100) + def get_equipped_item(self): return self.equipped_item - def set_strength(self, value): - self.strength = value + def set_strength(self, value:int): + self.strength = self.clamp(value, 0, 20) - def set_dexterity(self, value): - self.dexterity = value + def set_dexterity(self, value:int): + self.dexterity = self.clamp(value, 0, 20) - def set_intelligence(self, value): - self.intelligence = value + def set_intelligence(self, value:int): + self.intelligence = self.clamp(value, 0, 20) - def set_wisdom(self, value): - self.wisdom = value + def set_wisdom(self, value:int): + self.wisdom = self.clamp(value, 0, 20) - def set_charisma(self, value): - self.charisma = value + def set_charisma(self, value:int): + self.charisma = self.clamp(value, 0, 20) - def set_hp(self, value): - self.hp = value + def set_hp(self, value:int): + self.hp = self.clamp(value, 0, 100) - def set_armor(self, value): - self.armor = value + def set_armor(self, value:int): + self.armor = self.clamp(value, 0, 100) - def set_speed(self, value): - self.speed = value + def set_speed(self, value:int): + self.speed = self.clamp(value, 0, 100) - def set_equipped_item(self, item): + def set_equipped_item(self, item:Item): self.equipped_item = item + def deal_damage(self, dmg_amount:int): + current_hp = self.get_hp() + self.set_hp(current_hp - dmg_amount) diff --git a/entities/npc.py b/entities/npc.py index 5766dc8..c57cf7c 100644 --- a/entities/npc.py +++ b/entities/npc.py @@ -1,5 +1,7 @@ -from entity import Entity +# Game imports +from entities.entity import Entity class NPC(Entity): - def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed): - super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed) + def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item = None): + super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) + diff --git a/entities/player.py b/entities/player.py index 778eb3f..1bc6791 100644 --- a/entities/player.py +++ b/entities/player.py @@ -1,7 +1,6 @@ -from entity import Entity +# Game imports +from entities.entity import Entity class Player(Entity): def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None): super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) - - \ No newline at end of file diff --git a/events/event.py b/events/event.py index 5785df0..60e2259 100644 --- a/events/event.py +++ b/events/event.py @@ -1,14 +1,42 @@ # Game imports -from serializable import Serializable +from utils.serializable import Serializable from entities.entity import Entity +from turn import Turn, TurnAction # Native imports import json import uuid class Event(Serializable): - def __init__(self, location:str): + def __init__(self, location:str, initial_description:str, entities:list[str]): super().__init__() - self.id = str(uuid.uuid4()) - self.location = location - self.description = "" \ No newline at end of file + self.id:str = str(uuid.uuid4()) + self.location:str = location + self.initial_description:str = initial_description + + self.entities:list[str] = entities + self.turns:list[Turn] = [] + self.add_turn() + + def remove_entity(self, entity_id:str): + if entity_id in self.entities: + self.entities.remove(entity_id) + + def get_current_turn(self): + idx = len(self.turns) - 1 + if idx < 0: + raise IndexError("There is no turns yet, you should create one!") + return self.turns[idx] + + def add_turn(self): + self.turns.append(Turn()) + + def perform_action(self, action:TurnAction, entity_id:str, description:str): + current_turn = self.get_current_turn() + current_turn.add_action(action_type=action, entity_id=entity_id, description=description) + + if current_turn.is_finished(): + self.add_turn() + return True + + return False diff --git a/events/turn.py b/events/turn.py index 9c5a3ae..b6e1568 100644 --- a/events/turn.py +++ b/events/turn.py @@ -1,14 +1,21 @@ +# Game import +from utils.serializable import Serializable + # Native imports -from enum import Enum, IntEnum +from enum import Enum -class Action(Enum): - ATTACK = 'strength' # Physical Battle action - FORCE = 'strength' # Actions that requires physical effort - SPELL = 'intelligence' # Many kind of spell (battle or not) - SCAN = 'wisdom' # Danger in environment or NPC's lies - SPEECH = 'charisma' # To persuade or deceive - AGILE = 'dexterity' # Avoid traps or incoming attacks & spell +class TurnAction(Enum): + DAMAGE = 'deal_damage' + STATS = 'modify_stat' + BASIC = 'basic_action' -class BonusAction(IntEnum): - EQUIP_ITEM = 0 - USE_CONSUMMABLE = 1 \ No newline at end of file +class Turn(Serializable): + def __init__(self): + super().__init__() + self.actions = {} + + def add_action(self, action_type:TurnAction, entity_id:str, description:str): + self.actions[entity_id] = f'[{action_type.value}]: ' + description + + def is_finished(self, nb_entities:int): + return len(self.actions.keys()) == nb_entities \ No newline at end of file diff --git a/game.py b/game.py deleted file mode 100644 index 67ff05b..0000000 --- a/game.py +++ /dev/null @@ -1,43 +0,0 @@ -from serializable import Serializable -from utils.dice import Dice -from events.event import Event -from entities.player import Player -from entities.npc import NPC -from utils.item import Item - -class Game(Serializable): - def __init__(self, seed:int=42): - self.active_players:list[Player] = [] - self.active_npcs:list[NPC] = [] - self.active_items:list[Item] = [] - - self.events:list[Event] = [] - - def get_player(self, player_id:str): - for player in self.active_players: - if player.id == player_id: - return player - raise ReferenceError(f"The player #{player_id} doesn't exist!") - - def get_npc(self, npc_id:str): - for npc in self.active_npcs: - if npc.id == npc_id: - return npc - raise ReferenceError(f"The npc #{npc_id} doesn't exist!") - - def get_item(self, item_id:str): - for item in self.active_items: - if item.id == item_id: - return item - raise ReferenceError(f"The item #{item_id} doesn't exist!") - - def get_current_event(self): - idx = len(self.events) - 1 - if idx < 0: - raise IndexError("There is no event yet, you should create one!") - return self.events[idx] - - def add_event(self, new_event:Event): - self.events.append(new_event) - - #TODO: Add State Summary as Resource? \ No newline at end of file diff --git a/server.py b/server.py index 47c02f8..1eabac5 100644 --- a/server.py +++ b/server.py @@ -1,18 +1,26 @@ +# Game imports +from utils.dice import Dice +from utils.game import Game +from utils.serializable import Serializable +from events.event import Event + +# Native imports from typing import Any, Dict import logging import httpx from mcp.server.fastmcp import FastMCP -from utils.dice import Dice -from entities.player import Player -from items.item import Item -from game import Game -from entities.npc import NPC -from serializable import Serializable import json import os + +# Constants +HISTORY_FILE = "game_history.json" +SAVE_PATH = "save_" + +# Global Parameters mcp = FastMCP("wyvern-castle") game: Game = None +# Logging config logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -21,10 +29,7 @@ logging.basicConfig( ] ) -# Constants -HISTORY_FILE = "game_history.json" -SAVE_PATH = "save_" - +# SAVING & LOADING GAME STATE @mcp.tool() async def load_game(slot:int): """Loads an already existing game. @@ -69,6 +74,7 @@ async def save_game(slot:int): "error": str(e) } +@DeprecationWarning def append_to_history(event: Dict[str, Any]): """Append a game event to the history file.""" history = [] @@ -82,6 +88,7 @@ def append_to_history(event: Dict[str, Any]): with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) +@DeprecationWarning def read_history() -> list: """Read the game history from the file.""" if os.path.exists(HISTORY_FILE): @@ -92,6 +99,26 @@ def read_history() -> list: return [] return [] +# EVENTS TOOLS +@mcp.tool() +async def start_event(location:str, initial_description:str, entity_list:list[str]): + new_event = Event(location=location, initial_description=initial_description, entities=entity_list) + game.add_event(new_event) + +@mcp.tool() +async def perform_action(): + pass + +# ITEM TOOLS +@mcp.tool() +async def create_item(): + pass + +@mcp.tool() +async def add_item_to_entity(): + pass + +# OTHER UTILS @mcp.tool() async def throw_a_dice(n_faces: int) -> Any: """Throw a dice with n faces. If n==2 its a coin toss. @@ -102,18 +129,27 @@ async def throw_a_dice(n_faces: int) -> Any: logging.info(f"Throwing a dice with {n_faces} faces") if n_faces < 1: - raise ValueError("Number of faces must be at least 1") - elif n_faces == 1: - return 1 + return { + "success": False, + "error": "Number of faces must be at least 1" + } elif n_faces == 2: - return Dice.head_or_tails() + return { + "success": True, + "toss_result": Dice.head_or_tails() + } else: - return Dice.roll(n_faces) - - + return { + "success": True, + "roll_result": Dice.roll(n_faces) + } @mcp.tool() -async def create_player(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item: str = "") -> Dict[str, Any]: +async def get_entity_status(): + pass + +@mcp.tool() +async def create_player(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item_id:str): """Create a new player. Need all the stats to function properly. Throw a d20 for every stats you don't have, and a d6 for hp, armor and speed. @@ -129,14 +165,25 @@ async def create_player(name: str, strength: int, dexterity: int, intelligence: speed: Speed of the player item: Item carried by the player """ - logging.info(f"Creating player with name={name}") - player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed) - logging.info(f"Created player: {player}") - game.active_players.append(player) - return player.serialize_dict() + logging.info(f"Creating NPC named {name}") + try: + player_id = game.create_player(name=name, strength=strength, dexterity=dexterity, intelligence=intelligence, wisdom=wisdom, charisma=charisma, hp=hp, armor=armor, speed=speed) + game.add_item_to_entity(item_id=item_id, entity_id=player_id) + player_dict = game.get_player(player_id=player_id).serialize() + logging.info(f"Creation of NPC successful") + return { + "success": True, + "npc_properties": player_dict + } + except ReferenceError as e: + logging.info(f"ReferenceError: " + str(e)) + return { + "success": False, + "error": str(e) + } @mcp.tool() -async def create_npc(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item: str = "") -> Dict[str, Any]: +async def create_npc(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item_id:str): """Create a new NPC. Need all the stats to function properly. Throw a d20 for every stats you don't have, and a d6 for hp, armor and speed. @@ -152,14 +199,25 @@ async def create_npc(name: str, strength: int, dexterity: int, intelligence: int speed: Speed of the NPC item: Item carried by the NPC """ - logging.info(f"Creating NPC with name={name}") - npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed) - logging.info(f"Created NPC: {npc}") - game.active_npcs.append(npc) - return npc.serialize_dict() + logging.info(f"Creating NPC named {name}") + try: + npc_id = game.create_npc(name=name, strength=strength, dexterity=dexterity, intelligence=intelligence, wisdom=wisdom, charisma=charisma, hp=hp, armor=armor, speed=speed) + game.add_item_to_entity(item_id=item_id, entity_id=npc_id) + npc_dict = game.get_npc(npc_id=npc_id).serialize() + logging.info(f"Creation of NPC successful") + return { + "success": True, + "npc_properties": npc_dict + } + except ReferenceError as e: + logging.info(f"ReferenceError: " + str(e)) + return { + "success": False, + "error": str(e) + } @mcp.tool() -async def create_item(name: str, description: str, bonus: str) -> Dict[str, Any]: +async def create_item(name: str, description: str, stat_modifier: dict[str,int]): """Create a new item. Args: @@ -167,11 +225,13 @@ async def create_item(name: str, description: str, bonus: str) -> Dict[str, Any] description: Description of the item bonus: Bonus of the item ex: strength+1,hp+5 """ - logging.info(f"Creating item with name={name}") - item = Item(name, description, bonus) - logging.info(f"Created item: {item}") - game.active_items.append(item) - return item.serialize_dict() + logging.info(f"Creating item, name={name} ; description={description}") + item_id = game.create_item(name, description, stat_modifier) + item_dict = game.get_item(item_id).serialize() + return { + "success": True, + "item_properties": item_dict + } @mcp.tool() async def add_item_to_player(player_name: str, item_name: str) -> Dict[str, Any]: diff --git a/utils/game.py b/utils/game.py new file mode 100644 index 0000000..f8535af --- /dev/null +++ b/utils/game.py @@ -0,0 +1,215 @@ +# Game imports +from serializable import Serializable +from dice import Dice +from events.event import Event +from entities.player import Player +from entities.npc import NPC +from utils.item import Item +from events.turn import TurnAction + +# Native imports +from enum import IntEnum + +class Stat(IntEnum): + STRENGTH = 0, + INTELLIGENCE = 1, + DEXTERITY = 2, + WISDOM = 3, + CHARISMA = 4, + HP = 5, + ARMOR = 6, + SPEED = 7 + +class Game(Serializable): + def __init__(self, seed:int=42): + self.active_players:list[Player] = [] + self.active_npcs:list[NPC] = [] + self.active_items:list[Item] = [] + + self.events:list[Event] = [] + self.turn_order:list[str] = [] + self.turn_idx = 0 + + def get_entity(self, entity_id:str): + for entity in (self.active_players + self.active_npcs): + if entity.id == entity_id: + return entity + raise ReferenceError(f"The player #{entity_id} doesn't exist!") + + def get_player(self, player_id:str): + for player in self.active_players: + if player.id == player_id: + return player + raise ReferenceError(f"The player #{player_id} doesn't exist!") + + def create_player(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item=None): + new_player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) + self.active_players.append(new_player) + return new_player.id + + def get_npc(self, npc_id:str): + for npc in self.active_npcs: + if npc.id == npc_id: + return npc + raise ReferenceError(f"The npc #{npc_id} doesn't exist!") + + def create_npc(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item=None): + new_npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) + self.active_players.append(new_npc) + return new_npc.id + + def get_item(self, item_id:str): + for item in self.active_items: + if item.id == item_id: + return item + raise ReferenceError(f"The item #{item_id} doesn't exist!") + + def create_item(self,name:str, description:str, stat_modifier:dict[str, int]): + new_item = Item(name, description, stat_modifier) + self.active_items.append(new_item) + return new_item.id + + def add_player(self, new_player:Player): + if new_player.id in [player.id for player in self.active_players]: + raise ReferenceError(f"Player id #{new_player.id} already present in game!") + self.active_players.append(new_player) + + def add_ncp(self, new_ncp:NPC): + if new_ncp.id in [npc.id for npc in self.active_npcs]: + raise ReferenceError(f"NCP id #{new_ncp.id} already present in game!") + self.active_npcs.append(new_ncp) + + def add_item(self, new_item:Item): + if new_item.id in [item.id for item in self.active_items]: + raise ReferenceError(f"Item id #{new_item.id} already present in game!") + self.active_items.append(new_item) + + def add_item_to_entity(self, item_id:str, entity_id:str): + item = self.get_item(item_id) + entity = self.get_entity(entity_id) + + entity.set_equipped_item(item) + self.active_items.remove(item) + + def get_current_event(self): + idx = len(self.events) - 1 + if idx < 0: + raise IndexError("There is no event yet, you should create one!") + return self.events[idx] + + def add_event(self, new_event:Event): + self.events.append(new_event) + self.update_turn_order() + self.turn_idx = 0 + + def check_turn_ended(self): + if self.turn_idx == len(self.turn_order)-1: + # Turn end + self.get_current_event().add_turn() + self.update_turn_order() + + def update_turn_order(self): + active_entities = self.get_current_event().entities + entity_list = [self.get_entity(id) for id in active_entities] + + entity_list = self.sort_entities_by_speed(entity_list) + + self.turn_order = [entity.id for entity in entity_list] + self.turn_idx = 0 + + # Selection sort based on entity's speed + def sort_entities_by_speed(self, entity_list:list[NPC|Player]): + n = len(entity_list) + + for i in range(n): + max_idx = i # Current fastest entity + for j in range(i+1, n): + entity_max = entity_list[max_idx] + entity_j = entity_list[j] + + if entity_max.speed < entity_j.speed: + max_idx = j # New Maximum Speed for j entity + + # Swapping current index i with the new maximum in i+1, n-1 + entity_list[i], entity_list[max_idx] = entity_list[max_idx], entity_list[i] + + return entity_list + + def kill_entity(self, entity_id:str): + dead_entity = self.get_entity(entity_id) + self.get_current_event().remove_entity(entity_id=entity_id) + + if isinstance(dead_entity, NPC): + self.active_npcs.remove(dead_entity) + if isinstance(dead_entity, Player): + self.active_players.remove(dead_entity) + + def is_turn_coherent(self, entity_id:str): + return self.turn_order[self.turn_idx] == entity_id + + def deal_damage(self, src:str, target:str, roll:int, stat:int, description:str): + if not self.is_turn_coherent(src): + raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!") + + src_entity = self.get_entity(src) + target_entity = self.get_entity(target) + + dmg_amount = 0 + if stat == Stat.STRENGTH: # Strength damages physical and long closed range weapons + dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_strength() + elif stat == Stat.INTELLIGENCE: # Using magic + dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_intelligence() + elif stat == Stat.DEXTERITY: # Using daggers, bows or throws + dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_dexterity() + + target_entity.deal_damage(dmg_amount) + + additional_info = f"; {target_entity.name}({target_entity.get_id()}) took {dmg_amount} damage, {target_entity.get_hp()}hp remaining!" + + if target_entity.get_hp() <= 0: + self.kill_entity(target_entity) + additional_info = f"; {target_entity.name}({target_entity.get_id()}) took {dmg_amount} damage, {target_entity.name} died!" + + turn_finished = self.get_current_event().perform_action(TurnAction.DAMAGE, src, description=description+additional_info) + self.turn_idx += 1 + if turn_finished: + self.update_turn_order() + + def modifying_stat(self, src:str, value:int, stat:int, description:str): + if not self.is_turn_coherent(src): + raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!") + + src_entity = self.get_entity(src) + match(stat): + case Stat.STRENGTH: + src_entity.set_strength(src_entity.strength + value) + case Stat.INTELLIGENCE: + src_entity.set_intelligence(src_entity.intelligence + value) + case Stat.DEXTERITY: + src_entity.set_dexterity(src_entity.dexterity + value) + case Stat.WISDOM: + src_entity.set_wisdom(src_entity.wisdom + value) + case Stat.CHARISMA: + src_entity.set_charisma(src_entity.charisma + value) + + case Stat.HP: + src_entity.set_hp(src_entity.hp + value) + case Stat.ARMOR: + src_entity.set_armor(src_entity.armor + value) + case Stat.SPEED: + src_entity.set_speed(src_entity.speed + value) + + turn_finished = self.get_current_event().perform_action(TurnAction.STATS, src, description=description) + self.turn_idx += 1 + if turn_finished: + self.update_turn_order() + + def simple_action(self, src:str, description:str): + if not self.is_turn_coherent(src): + raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!") + + turn_finished = self.get_current_event().perform_action(TurnAction.BASIC, src, description=description) + self.turn_idx += 1 + if turn_finished: + self.update_turn_order() + #TODO: Add State Summary as Resource? \ No newline at end of file diff --git a/utils/item.py b/utils/item.py index 7273f75..b7c4d6c 100644 --- a/utils/item.py +++ b/utils/item.py @@ -1,10 +1,11 @@ -from typing import Dict +# Game imports from serializable import Serializable +# Native imports import uuid class Item(Serializable): - def __init__(self,name:str, description:str, stat_modifier:Dict[str, int]): + def __init__(self,name:str, description:str, stat_modifier:dict[str, int]): super().__init__() self.id = str(uuid.uuid4()) self.name = name @@ -13,4 +14,3 @@ class Item(Serializable): def __str__(self): return f"{self.name}: {self.description}" - diff --git a/serializable.py b/utils/serializable.py similarity index 99% rename from serializable.py rename to utils/serializable.py index e39d462..b1451d2 100644 --- a/serializable.py +++ b/utils/serializable.py @@ -1,3 +1,4 @@ +# Native imports import json from typing import Any, Dict, List, Type, TypeVar