diff --git a/.gitignore b/.gitignore index 3dc82e5..ad1ef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .python-version __pycache__/ uv.lock + +# Save/Load files for testing *.json \ No newline at end of file diff --git a/__pycache__/dice.cpython-312.pyc b/__pycache__/dice.cpython-312.pyc deleted file mode 100644 index b836c8e..0000000 Binary files a/__pycache__/dice.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/entity.cpython-312.pyc b/__pycache__/entity.cpython-312.pyc deleted file mode 100644 index 059185e..0000000 Binary files a/__pycache__/entity.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/game.cpython-312.pyc b/__pycache__/game.cpython-312.pyc deleted file mode 100644 index 44d7dae..0000000 Binary files a/__pycache__/game.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/inventory.cpython-312.pyc b/__pycache__/inventory.cpython-312.pyc deleted file mode 100644 index 2ea535d..0000000 Binary files a/__pycache__/inventory.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/item.cpython-312.pyc b/__pycache__/item.cpython-312.pyc deleted file mode 100644 index 5b84d0b..0000000 Binary files a/__pycache__/item.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/npc.cpython-312.pyc b/__pycache__/npc.cpython-312.pyc deleted file mode 100644 index 7bef8dc..0000000 Binary files a/__pycache__/npc.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/player.cpython-312.pyc b/__pycache__/player.cpython-312.pyc deleted file mode 100644 index 2b0a4b3..0000000 Binary files a/__pycache__/player.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/serializable.cpython-312.pyc b/__pycache__/serializable.cpython-312.pyc deleted file mode 100644 index b53dcac..0000000 Binary files a/__pycache__/serializable.cpython-312.pyc and /dev/null differ diff --git a/dice.py b/dice.py index 59cd6f7..eb68ce2 100644 --- a/dice.py +++ b/dice.py @@ -1,13 +1,13 @@ +# Native imports import random as rd class Dice(): - def __init__(self, num_faces:int=20): - self.num_faces = num_faces + @staticmethod + def roll(num_faces=20): + return rd.randrange(start=1, stop=num_faces+1, step=1) - def roll(self): - return rd.randrange(start=1, stop=self.num_faces+1, step=1) - - def head_or_tails(self): + @staticmethod + def head_or_tails(): result = rd.randint(0,1) if result: # true return "head" # face diff --git a/entities/entity.py b/entities/entity.py new file mode 100644 index 0000000..6994a1c --- /dev/null +++ b/entities/entity.py @@ -0,0 +1,22 @@ +# Game imports +from serializable import Serializable + +# Native imports +import uuid + +class Entity(Serializable): + def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_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.equipped_item = equipped_item + + def get_id(self): + return self.id \ No newline at end of file diff --git a/npc.py b/entities/npc.py similarity index 60% rename from npc.py rename to entities/npc.py index 8610fb7..5766dc8 100644 --- a/npc.py +++ b/entities/npc.py @@ -1,5 +1,5 @@ from entity import Entity class NPC(Entity): - def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, ): + def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed): super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed) diff --git a/player.py b/entities/player.py similarity index 100% rename from player.py rename to entities/player.py diff --git a/entity.py b/entity.py deleted file mode 100644 index 4058183..0000000 --- a/entity.py +++ /dev/null @@ -1,30 +0,0 @@ -from serializable import Serializable -from enum import Enum, IntEnum - -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 BonusAction(IntEnum): - EQUIP_ITEM = 0 - USE_CONSUMMABLE = 1 - -class Entity(Serializable): - def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None): - 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.equipped_item = equipped_item - - def make_a_turn(self, action:Action, bonus_action:BonusAction): - pass \ No newline at end of file diff --git a/event.py b/event.py deleted file mode 100644 index 58b067f..0000000 --- a/event.py +++ /dev/null @@ -1,26 +0,0 @@ -from serializable import Serializable -from entity import Action, BonusAction, Entity - -import json - -class Event(Serializable): - def __init__(self, location:str, entities_objectives:dict[str,str]): - super().__init__() - self.location = location - self.entities_objectives = entities_objectives - self.description = "" - - def add_turn_event(self, event_text:str, action:Action, bonus:BonusAction): - event_dict = {} - event_dict['text'] = event_text - event_dict['action'] = action - event_dict['bonus'] = bonus - self.description += json.dumps(event_dict) - - def update_objectives(self, entities:list[Entity], objectives:list[str]): - assert len(entities) == len(objectives), f"Number of entities & objectives are different: {len(entities)} (entities) and {len(objectives)} (objectives) !" - n = len(entities) - for i in range(n): - entity_name = entities[i].name - objective = objectives[i] - self.entities_objectives[entity_name] = objective \ No newline at end of file diff --git a/events/event.py b/events/event.py new file mode 100644 index 0000000..5785df0 --- /dev/null +++ b/events/event.py @@ -0,0 +1,14 @@ +# Game imports +from serializable import Serializable +from entities.entity import Entity + +# Native imports +import json +import uuid + +class Event(Serializable): + def __init__(self, location:str): + super().__init__() + self.id = str(uuid.uuid4()) + self.location = location + self.description = "" \ No newline at end of file diff --git a/events/turn.py b/events/turn.py new file mode 100644 index 0000000..9c5a3ae --- /dev/null +++ b/events/turn.py @@ -0,0 +1,14 @@ +# Native imports +from enum import Enum, IntEnum + +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 BonusAction(IntEnum): + EQUIP_ITEM = 0 + USE_CONSUMMABLE = 1 \ No newline at end of file diff --git a/game.py b/game.py index 6913e4b..8505717 100644 --- a/game.py +++ b/game.py @@ -1,10 +1,43 @@ from serializable import Serializable from dice import Dice +from events.event import Event +from entities.player import Player +from entities.npc import NPC +from items.item import Item class Game(Serializable): def __init__(self, seed:int=42): - self.players = [] - self.npcs = [] - self.items = [] + self.active_players:list[Player] = [] + self.active_npcs:list[NPC] = [] + self.active_items:list[Item] = [] - \ No newline at end of file + 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/inventory.py b/inventory.py deleted file mode 100644 index 1f748d1..0000000 --- a/inventory.py +++ /dev/null @@ -1,39 +0,0 @@ -from serializable import Serializable -from item import Item - -class Inventory(Serializable): - def __init__(self, max_capacity:float = 50.0): - super().__init__() - self.items:list[Item] = [] - self.max_capacity = max_capacity # Weight (kg) - - def current_capacity(self): - return sum([item.weight for item in self.items]) - - def list_items(self): - s_items = '' - for item in self.items: - s_items += item.__str__() + '; ' - return s_items - - def add_item(self, added_item:Item): - if added_item.weight + self.current_capacity() > self.max_capacity: - return f'{added_item.name} is too heavy to fit inside the inventory: {added_item.weight + self.current_capacity() - self.max_capacity}kg in surplus!' - else: - self.items.append(added_item) - return f'{added_item.name} added to inventory. Current items load: {self.current_capacity()}kg' - - def remove_item(self, r_item): - if r_item in self.items: - self.items.remove(r_item) - return f'{r_item.name} was removed from the inventory' - else: - return f'{r_item.name} is not present within the inventory' - - def get_item(self, item_name:str): - for item in self.items: - if item.name == item_name: - return item - - # The item was not found - return None \ No newline at end of file diff --git a/items/inventory.py b/items/inventory.py new file mode 100644 index 0000000..41088f7 --- /dev/null +++ b/items/inventory.py @@ -0,0 +1,37 @@ +from serializable import Serializable +from items.item import Item + +class Inventory(Serializable): + def __init__(self, max_capacity:int = 5): + super().__init__() + self.items:list[Item] = [] + self.max_capacity = max_capacity # Maximum umber of items + + def current_capacity(self): + return len(self.items) + + def list_items(self): + s_items = '' + for item in self.items: + s_items += item.__str__() + '; ' + return s_items + + def add_item(self, added_item:Item): + if self.current_capacity() == self.max_capacity: + return f'The inventory is full!' + else: + self.items.append(added_item) + return f'{added_item.name} added to inventory. Current number of items: {self.current_capacity()}/{self.max_capacity}' + + def remove_item(self, item_id:str): + searched_item = self.get_item(item_id=item_id) + if searched_item: + return f'{searched_item.name} was removed from the inventory.' + raise ValueError(f'Item #{item_id} is not present within the inventory!') + + def get_item(self, item_id:str): + for item in self.items: + if item.id == item_id: + return item + # The item was not found + return None \ No newline at end of file diff --git a/item.py b/items/item.py similarity index 70% rename from item.py rename to items/item.py index 0888990..5202eae 100644 --- a/item.py +++ b/items/item.py @@ -1,19 +1,21 @@ from serializable import Serializable +import uuid + class Item(Serializable): - def __init__(self,name:str, description:str, stat_modifier:str, weight:float = 10.0): + def __init__(self,name:str, description:str, stat_modifier:str): super().__init__() + self.id = str(uuid.uuid4()) self.name = name self.description = description self.stat_modifier = stat_modifier - self.weight = weight def __str__(self): - return f"{self.name} ({self.weight} kg): {self.description}" + return f"{self.name}: {self.description}" class Equippable(Item): - def __init__(self, name, description, stat_modifier, equipped:bool, weight = 10.0): - super().__init__(name, description, stat_modifier, weight) + def __init__(self, name, description, stat_modifier, equipped:bool): + super().__init__(name, description, stat_modifier) self.equipped = equipped def equip(self): @@ -23,8 +25,8 @@ class Equippable(Item): self.equipped = False class Consummable(Item): - def __init__(self, name, description, stat_modifier, nb_of_uses:int, weight = 10.0): - super().__init__(name, description, stat_modifier, weight) + def __init__(self, name, description, stat_modifier, nb_of_uses:int): + super().__init__(name, description, stat_modifier) self.nb_of_uses = nb_of_uses def consumme(self): diff --git a/main.py b/main.py deleted file mode 100644 index 5c5d443..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from mcp-nlp!") - - -if __name__ == "__main__": - main() diff --git a/server.py b/server.py index e1d4da4..5f56ef0 100644 --- a/server.py +++ b/server.py @@ -11,7 +11,7 @@ from serializable import Serializable import json import os mcp = FastMCP("wyvern-castle") -game = Game() +game: Game = None logging.basicConfig( level=logging.INFO, @@ -21,8 +21,53 @@ logging.basicConfig( ] ) -# History file path +# Constants HISTORY_FILE = "game_history.json" +SAVE_PATH = "save_" + +@mcp.tool() +async def load_game(slot:int): + """Loads an already existing game. + + Args: + slot: Integer id of the save slot. + """ + global game + path = SAVE_PATH + str(slot) + ".json" + try: + with open(path, "r", encoding="utf-8") as f: + game.deserialize(f.read()) + return { + "success": True, + "msg": f"{path} as been successfully loaded!" + } + except OSError as e: + return { + "success": False, + "error": str(e) + } + +@mcp.tool() +async def save_game(slot:int): + """Saves the current game to the given slot. + + Args: + slot: Integer id of the save slot. + """ + global game + path = SAVE_PATH + str(slot) + ".json" + try: + with open(path, "w", encoding="utf-8") as f: + f.write(game.serialize()) + return { + "success": True, + "msg": f"{path} as been successfully saved!" + } + except OSError as e: + return { + "success": False, + "error": str(e) + } def append_to_history(event: Dict[str, Any]): """Append a game event to the history file."""