diff --git a/entities/entity.py b/entities/entity.py index 0ff8eb5..96abb29 100644 --- a/entities/entity.py +++ b/entities/entity.py @@ -6,7 +6,7 @@ from utils.item import Item import uuid class Entity(Serializable): - 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): + def __init__(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item): self.id = str(uuid.uuid4()) self.name = name self.strength = 20 diff --git a/events/event.py b/events/event.py index 60e2259..83880e4 100644 --- a/events/event.py +++ b/events/event.py @@ -1,7 +1,7 @@ # Game imports from utils.serializable import Serializable from entities.entity import Entity -from turn import Turn, TurnAction +from events.turn import Turn, TurnAction # Native imports import json @@ -35,7 +35,7 @@ class Event(Serializable): current_turn = self.get_current_turn() current_turn.add_action(action_type=action, entity_id=entity_id, description=description) - if current_turn.is_finished(): + if current_turn.is_finished(len(self.entities)): self.add_turn() return True diff --git a/server.py b/server.py index 1eabac5..6d5c4c9 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ # Game imports +from events.turn import TurnAction from utils.dice import Dice -from utils.game import Game +from utils.game import Game, Stat from utils.serializable import Serializable from events.event import Event @@ -18,7 +19,7 @@ SAVE_PATH = "save_" # Global Parameters mcp = FastMCP("wyvern-castle") -game: Game = None +game = Game() # Logging config logging.basicConfig( @@ -74,49 +75,91 @@ 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 = [] - if os.path.exists(HISTORY_FILE): - with open(HISTORY_FILE, "r", encoding="utf-8") as f: - try: - history = json.load(f) - except json.JSONDecodeError: - history = [] - history.append(event) - 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): - with open(HISTORY_FILE, "r", encoding="utf-8") as f: - try: - return json.load(f) - except json.JSONDecodeError: - return [] - return [] # EVENTS TOOLS @mcp.tool() async def start_event(location:str, initial_description:str, entity_list:list[str]): + """ Start a new event in the game. + Args: + location: Location of the event + initial_description: Initial description of the event + entity_list: List of entity IDs involved in the event + """ new_event = Event(location=location, initial_description=initial_description, entities=entity_list) game.add_event(new_event) @mcp.tool() -async def perform_action(): - pass +async def perform_attack(src_entity_id:str, target_entity_id:str, attack_type:Stat): + """Perform an attack during an event. This add an attack action to the current event. + Args: + src_entity_id: The ID of the entity performing the attack + target_entity_id: The ID of the entity being attacked + attack_type: The type of attack being performed using the class Stat, can be Stat.STRENGTH, Stat.INTELLIGENCE, Stat.DEXTERITY which are 0, 1 and 2 respectively + """ + logging.info(f"Entity {src_entity_id} is performing an attack on {target_entity_id} using {attack_type}") + game.deal_damage(src=src_entity_id, target=target_entity_id, roll=Dice.roll(20), stat=attack_type, description=f"Entity {src_entity_id} attacks {target_entity_id} with a {attack_type} based attack.") + return { + "success": True, + "msg": f"Attack performed by {src_entity_id} on {target_entity_id} using {attack_type}" + } + +@mcp.tool() +async def perform_test_action(entity_id:str, stat:Stat, difficulty:int, roll:int): + """Perform a test action during an event. This add a test action to the current event. + This can be opening a door (DEXTERITY), solving a puzzle (INTELLIGENCE), resisting a charm (WISDOM) etc. + Args: + entity_id: The ID of the entity performing the test + stat: The stat being tested (using the Stat enum) + difficulty: The difficulty of the test + """ + logging.info(f"Entity {entity_id} is performing a test action on stat {stat} with difficulty {difficulty}") + entity = game.get_entity(entity_id) + stat_value = 0 + match(stat): + case Stat.STRENGTH: + stat_value = entity.get_strength() + case Stat.INTELLIGENCE: + stat_value = entity.get_intelligence() + case Stat.DEXTERITY: + stat_value = entity.get_dexterity() + case Stat.WISDOM: + stat_value = entity.get_wisdom() + case Stat.CHARISMA: + stat_value = entity.get_charisma() + total = roll + stat_value + success = total >= difficulty + description = f"Entity {entity_id} performs a {stat.name} test (roll: {roll} + stat: {stat_value} = total: {total}) against difficulty {difficulty}. " + if success: + description += "The test is successful." + else: + description += "The test fails." + game.get_current_event().perform_action(TurnAction.BASIC, entity_id, description=description) + return { + "success": success, + "total": total, + "msg": description + } + +@mcp.tool() +async def perform_stat_modification(entity_id:str, stat:Stat, value:int): + """Modify a stat of an entity during an event. This add a stat modification action to the current event. + This can be due to a spell, a potion, a curse etc. + Args: + entity_id: The ID of the entity whose stat is being modified + stat: The stat being modified (using the Stat enum) + value: The value to modify the stat by (can be positive or negative) + """ + logging.info(f"Entity {entity_id} is having its stat {stat} modified by {value}") + game.modifying_stat(src=entity_id, value=value, stat=stat, description=f"Entity {entity_id} has its {stat.name} modified by {value}.") + return { + "success": True, + "msg": f"Stat {stat.name} of entity {entity_id} modified by {value}" + } + + # ITEM TOOLS -@mcp.tool() -async def create_item(): - pass -@mcp.tool() -async def add_item_to_entity(): - pass # OTHER UTILS @mcp.tool() @@ -146,12 +189,21 @@ async def throw_a_dice(n_faces: int) -> Any: @mcp.tool() async def get_entity_status(): - pass + """ + Get the status of all entities in the game. + """ + logging.info("Getting status of all entities") + players = [player.serialize() for player in game.active_players] + npcs = [npc.serialize() for npc in game.active_npcs] + return { + "players": players, + "npcs": npcs + } @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. + and a d50 for hp, armor and speed. Args: name: Name of the player @@ -165,15 +217,24 @@ 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 NPC named {name}") + logging.info(f"Creating player 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) + player_id = game.create_player(name=name, + strength=strength, + dexterity=dexterity, + intelligence=intelligence, + wisdom=wisdom, + charisma=charisma, + hp=50 + hp, + armor=50 + armor, + speed= 50 + speed, + equipped_item=game.get_item(item_id)) # Check if item exists 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") + logging.info(f"Creation of player successful") return { "success": True, - "npc_properties": player_dict + "player_properties": player_dict } except ReferenceError as e: logging.info(f"ReferenceError: " + str(e)) @@ -185,7 +246,7 @@ async def create_player(name: str, strength: int, dexterity: int, intelligence: @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_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. + and a d100 for hp, armor and speed. Args: name: Name of the NPC @@ -201,7 +262,8 @@ async def create_npc(name: str, strength: int, dexterity: int, intelligence: int """ 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) + item = game.get_item(item_id) # Check if item exists + npc_id = game.create_npc(name=name, strength=strength, dexterity=dexterity, intelligence=intelligence, wisdom=wisdom, charisma=charisma, hp=50 + hp, armor=50 + armor, speed=50 + speed, equipped_item=item) 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") @@ -223,7 +285,7 @@ async def create_item(name: str, description: str, stat_modifier: dict[str,int]) Args: name: Name of the item description: Description of the item - bonus: Bonus of the item ex: strength+1,hp+5 + bonus: Bonus or malus of the item ex: {"strength":+1,"hp":-5} """ logging.info(f"Creating item, name={name} ; description={description}") item_id = game.create_item(name, description, stat_modifier) @@ -234,15 +296,82 @@ async def create_item(name: str, description: str, stat_modifier: dict[str,int]) } @mcp.tool() -async def add_item_to_player(player_name: str, item_name: str) -> Dict[str, Any]: - """Add an item to a player's inventory. +async def equip_item_to_entity(entity_id: str, item_id: str) -> Dict[str, Any]: + """Equip an item to an entity (player or NPC). Args: - player_name: The name of the player to add the item to - item_name: The name of the item to add + entity_id: The id of the entity to equip the item to + item_id: The id of the item to equip """ - logging.info(f"Adding item {item_name} to player {player_name}") - return {"status": "Item added"} + logging.info(f"Equipping item {item_id} to entity {entity_id}") + game.add_item_to_entity(item_id=item_id, entity_id=entity_id) + return {"status": "Item equipped"} + +@mcp.tool() +async def get_entity_info(entity_id: str) -> Dict[str, Any]: + """Get the full information of an entity (player or NPC). + + Args: + entity_id: The id of the entity to get information from + """ + logging.info(f"Getting info for entity {entity_id}") + entity = game.get_entity(entity_id) + return { + "entity_properties": entity.serialize() + } + + +@mcp.tool() +async def get_item_info(item_id: str) -> Dict[str, Any]: + """Get the full information of an item. + + Args: + item_id: The id of the item to get information from + """ + logging.info(f"Getting info for item {item_id}") + item = game.get_item(item_id) + return { + "item_properties": item.serialize() + } + +@mcp.tool() +async def get_game_state() -> str: + """Get the current game state as a serialized string.""" + logging.info("Getting current game state") + return game.serialize() + + +@mcp.tool() +async def get_current_event() -> Dict[str, Any]: + """Get the current event in the game.""" + logging.info("Getting current event") + event = game.get_current_event() + if event: + return { + "success": True, + "event_properties": event.serialize() + } + else: + return { + "success": False, + "error": "No current event" + } + +@mcp.tool() +async def add_event(location:str, initial_description:str, entity_list:list[str]) -> str: + """Add a new event to the game. + + Args: + location: Location of the event + initial_description: Initial description of the event + entity_list: List of entity IDs involved in the event + """ + logging.info("Adding new event to the game") + new_event = Event(location=location, initial_description=initial_description, entities=entity_list) + game.add_event(new_event) + return "Event added successfully." + + @mcp.tool() async def save_game_state() -> str: @@ -254,18 +383,7 @@ async def save_game_state() -> str: return "Game state saved to game_state.json" -# Example MCP tool to add an event to history -@mcp.tool() -async def add_event_to_history(event: Dict[str, Any]) -> str: - """Add a game event to the history resource.""" - append_to_history(event) - return "Event added to history." -# Example MCP tool to read history -@mcp.tool() -async def get_game_history() -> list: - """Get the full game history.""" - return read_history() @mcp.tool() async def purge_game_history_and_state() -> str: diff --git a/utils/game.py b/utils/game.py index f8535af..e76e958 100644 --- a/utils/game.py +++ b/utils/game.py @@ -1,6 +1,6 @@ # Game imports -from serializable import Serializable -from dice import Dice +from utils.serializable import Serializable +from utils.dice import Dice from events.event import Event from entities.player import Player from entities.npc import NPC @@ -11,13 +11,13 @@ from events.turn import TurnAction from enum import IntEnum class Stat(IntEnum): - STRENGTH = 0, - INTELLIGENCE = 1, - DEXTERITY = 2, - WISDOM = 3, - CHARISMA = 4, - HP = 5, - ARMOR = 6, + STRENGTH = 0 + INTELLIGENCE = 1 + DEXTERITY = 2 + WISDOM = 3 + CHARISMA = 4 + HP = 5 + ARMOR = 6 SPEED = 7 class Game(Serializable): @@ -42,7 +42,7 @@ class Game(Serializable): 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): + 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): new_player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) self.active_players.append(new_player) return new_player.id @@ -53,9 +53,9 @@ class Game(Serializable): 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): + 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): new_npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) - self.active_players.append(new_npc) + self.active_npcs.append(new_npc) return new_npc.id def get_item(self, item_id:str): @@ -162,12 +162,12 @@ class Game(Serializable): 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) + target_entity.deal_damage(int(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) + self.kill_entity(target_entity.get_id()) 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) diff --git a/utils/item.py b/utils/item.py index b7c4d6c..3760e3b 100644 --- a/utils/item.py +++ b/utils/item.py @@ -1,5 +1,5 @@ # Game imports -from serializable import Serializable +from utils.serializable import Serializable # Native imports import uuid