From 0504e90754f97957a635879d703d51789abf2aa5 Mon Sep 17 00:00:00 2001 From: Hemithermos Date: Mon, 26 Jan 2026 10:11:44 +0100 Subject: [PATCH] tweaks for server using serialization --- .gitignore | 5 +- __pycache__/dice.cpython-312.pyc | Bin 1060 -> 996 bytes __pycache__/game.cpython-312.pyc | Bin 546 -> 625 bytes __pycache__/serializable.cpython-312.pyc | Bin 4504 -> 4586 bytes dice.py | 14 ++-- game.py | 6 +- serializable.py | 6 +- server.py | 102 ++++++++++++++++------- 8 files changed, 90 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 207a3c8..3dc82e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # UV properties .venv/ -.python-version \ No newline at end of file +.python-version +__pycache__/ +uv.lock +*.json \ No newline at end of file diff --git a/__pycache__/dice.cpython-312.pyc b/__pycache__/dice.cpython-312.pyc index 8ce3fc6b40d05d048d329d744962a4e35b3493c7..b836c8e6d50014bfa0016b1cbd7f0d00c0f69add 100644 GIT binary patch delta 499 zcmZ3&@r0fCG%qg~0}yOeD$i_~$Qxa+2INd z0Lfb{Mfo{7Rh%Gi=z*AEc{!j=1H&DD!Ty@gngxn0RMzXQ)Y~C=QOoLp;1z!73mncx zpfF1YnF9kLhk!k624qa1oX@1g%vi%Xc^;FY2ou=WTWm$C#icnVQ19Jk(&PadQY-}| zfR;@bV)hce#hjT}QUvmK5h%=xBqo7YKi508*dW7+GByKeJ6f p%B&;>axK_BU?2VBu*uC&Da}c>D^i%u$Rfte!zlfU0Z4$A005)ZXWRe) delta 528 zcmaFDzJ!DKG%qg~0}!~Hmu2pr$Qxa60_03*NM(p(Oks#(N?~kah+OlK&8%cU@`hO(O&HJSWM zRe|!Mc?v0qtyU`Jr79GpmX;_KBo-?iUR6?*s*ssiT#}fVoOyUhU{ zqWmHthov|*C#@J{*u-~kdVD~-f#D9H_yr03%X|(OI2=9$MHrHqKqM5f0%@?LEGEY? zsxdN7p229O!E}qMDCHJsQDR;S5T~bVG8M4{)!pLEE6t5hOH58JE(V#QF!?p3jEW?X z)xhwCU$DQXvu1(f3e63UE46nxUg5X9z+qPe3eRMQ$p%cW%nUUQlPj1E`514p6{QxJ z=9GXPI(aXXCJ#4IRk0Mrpvj+@yrhaaft*`BK%bUmCg-M>WaOt52?K?SBq#ea%NZH~ znJo;Tm>5|d89%TwunB>f_Me%7tPda-SnMMcE2}hE>?70UR%RtBd5{4h2NiJuiC-Kx bx%nxjIjMFp*k28J8rk{v8PJU2wdfE4Eq zQSlCzUarYbjF!B7K>3vnMf^ajh<9=~qZAJ_tHccFFAPAch=1}%MtvtykQ_t-koAkh tCO1E&G$+-rNCLaeM#j4gs&^Sgzp$_{N_5zKWdKrOjR17fG~ECI delta 166 zcmey!vWSKEG%qg~0}!kUF3aql$h%hz#9;>F&n!SfU;+@2#?}Q`|RP>9(CO1E&G$+-r mNEFCr1ma?GAn}2jk&*E(gX&!d(Jw4ajJzE-Um1WD*kAzbz#w`6 diff --git a/__pycache__/serializable.cpython-312.pyc b/__pycache__/serializable.cpython-312.pyc index 55acc43862577efda32ab4a57e28258c5fe0cef8..b53dcacebf7a1b6bf760975b119f085d598526e6 100644 GIT binary patch delta 417 zcmbQC{7RYkG%qg~0}wnGFVBpd$a|czVd7OAPwo`vRQ5Ec6t))DC@vtIEtNf$BaKNC zs1PX1-og^aox%ZD!3kvZq%fhW-~x*BRr?oW|Z_xW&+s(1sp({8HhitFiw8V zw1uUHu}E_AbY=rSMurkjn4&D6$&K8?oGdAJG;% z{B{>O?0$Y`XAn$gEanDUv{BNDhwUIYw-e9iUF^G<7!@a%a%nT_PhQC7#3;Y{36~us zn?6ukk?CX&9vep8&8a*&OhD;Ne2I)klTG;Vvlf8@WU`1r5VsLXoeYSum|Q5J%ve5o zs(=OGM;?Aw`vu9L8GzIWrO9^%a->{AYD7VVFp&7gVUwGmQks)$SL8l9Sx|%5hEeMi J1CRhK0RTV;Xxsn* delta 323 zcmaE*JVTlHG%qg~0}!~Hmt}HK~8dC~u3riGN3Uex3DtjtN8j~bY z8BmlBD9W9}4pzYdWb>pjp{d{mit<)+X>xClVk~Bye2z(s=>x;$XG~ir&to>&{Ezt= z6C>N?r)=tzh1ioA3np7~%S@igZmoBRU%0=jvucLw0+9`_2NbXHyIkOK`T3cdK`@!I zm>X#MMoC8_wu9W1`z#0ZnC~$4x`KDIf4?5Zj-kPYVcYyYJFk=5=EjwtpFvrRgVAw diff --git a/dice.py b/dice.py index fd91518..59cd6f7 100644 --- a/dice.py +++ b/dice.py @@ -1,15 +1,13 @@ import random as rd class Dice(): - def __init__(self): - raise TypeError("Un dé ne peut pas être instanciée!") - - @staticmethod - def roll(self, num_faces=20): - return rd.randrange(start=1, stop=num_faces+1, step=1) + def __init__(self, num_faces:int=20): + self.num_faces = num_faces - @staticmethod - def head_or_tails(): + def roll(self): + return rd.randrange(start=1, stop=self.num_faces+1, step=1) + + def head_or_tails(self): result = rd.randint(0,1) if result: # true return "head" # face diff --git a/game.py b/game.py index d3be329..6913e4b 100644 --- a/game.py +++ b/game.py @@ -3,4 +3,8 @@ from dice import Dice class Game(Serializable): def __init__(self, seed:int=42): - pass \ No newline at end of file + self.players = [] + self.npcs = [] + self.items = [] + + \ No newline at end of file diff --git a/serializable.py b/serializable.py index d6d1ca7..e39d462 100644 --- a/serializable.py +++ b/serializable.py @@ -11,7 +11,7 @@ class Serializable: instance.deserialize_dict(data) return instance - def serialize(self, file) -> str: + def serialize(self, file=None) -> str: """Serializes the object and all nested Serializable objects to JSON.""" def serialize_value(value: Any) -> Any: if isinstance(value, Serializable): @@ -24,7 +24,9 @@ class Serializable: return value attrs = {k: serialize_value(v) for k, v in self.__dict__.items() if not k.startswith('_')} - return json.dumps(attrs, file, ensure_ascii=False, indent=2) + if file: + json.dump(attrs, file, ensure_ascii=False, indent=2) + return json.dumps(attrs, ensure_ascii=False, indent=2) def serialize_dict(self) -> Dict[str, Any]: """Serializes the object to a dictionary (for nested serialization).""" diff --git a/server.py b/server.py index 207e08b..e1d4da4 100644 --- a/server.py +++ b/server.py @@ -7,7 +7,9 @@ from player import Player from item import Item from game import Game from npc import NPC - +from serializable import Serializable +import json +import os mcp = FastMCP("wyvern-castle") game = Game() @@ -19,6 +21,32 @@ logging.basicConfig( ] ) +# History file path +HISTORY_FILE = "game_history.json" + +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) + +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 [] + @mcp.tool() async def throw_a_dice(n_faces: int) -> Any: """Throw a dice with n faces. If n==2 its a coin toss. @@ -27,7 +55,7 @@ async def throw_a_dice(n_faces: int) -> Any: n_faces: Number of faces of the dice """ logging.info(f"Throwing a dice with {n_faces} faces") - dice = Dice() + dice = Dice(n_faces) if n_faces < 1: raise ValueError("Number of faces must be at least 1") @@ -36,36 +64,12 @@ async def throw_a_dice(n_faces: int) -> Any: elif n_faces == 2: return dice.head_or_tails() else: - return dice.roll(n_faces) + return dice.roll() + + @mcp.tool() -async def mult(a: int, b: int) -> int: - """Multiply two numbers. - - Args: - a: First number - b: Second number - """ - logging.info(f"Calling mult with a={a}, b={b}") - result = a * b - logging.info(f"mult result: {result}") - return result - -@mcp.tool() -async def add(a: int, b: int) -> int: - """Add two numbers. - - Args: - a: First number - b: Second number - """ - logging.info(f"Calling add with a={a}, b={b}") - result = a + b - logging.info(f"add result: {result}") - return result - -@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 = None) -> Dict[str, Any]: +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]: """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. @@ -84,10 +88,11 @@ async def create_player(name: str, strength: int, dexterity: int, intelligence: 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.players.append(player) return player.serialize_dict() @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 = None) -> 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: str = "") -> Dict[str, Any]: """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. @@ -106,6 +111,7 @@ async def create_npc(name: str, strength: int, dexterity: int, intelligence: int 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.npcs.append(npc) return npc.serialize_dict() @mcp.tool() @@ -120,6 +126,7 @@ async def create_item(name: str, description: str, bonus: str) -> Dict[str, Any] logging.info(f"Creating item with name={name}") item = Item(name, description, bonus) logging.info(f"Created item: {item}") + game.items.append(item) return item.serialize_dict() @mcp.tool() @@ -133,6 +140,39 @@ async def add_item_to_player(player_name: str, item_name: str) -> Dict[str, Any] logging.info(f"Adding item {item_name} to player {player_name}") return {"status": "Item added"} +@mcp.tool() +async def save_game_state() -> str: + """Save the current game state to a persistent storage each time + the game state is modified.""" + logging.info("Saving game state") + with open("game_state.json", "w", encoding="utf-8") as f: + f.write(game.serialize()) + 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: + """Purge the game history and state files when the player is starting a new game.""" + if os.path.exists(HISTORY_FILE): + os.remove(HISTORY_FILE) + if os.path.exists("game_state.json"): + os.remove("game_state.json") + return "Game history and state purged." + + def main(): # Initialize and run the server mcp.run(transport="stdio")