21 Commits
api ... main

Author SHA1 Message Date
KuMiShi
1b1146a892 Readme fixes 2026-01-31 23:15:51 +01:00
KuMiShi
74f0df49bd Event modifs and README update 2026-01-31 23:13:26 +01:00
a6391f961d rebalance armor and hp limits 2026-01-31 18:06:15 +01:00
05f58cdbf5 Merge branch 'main' of https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle 2026-01-31 18:02:34 +01:00
e129815248 balance initial heath points for players and NPCs 2026-01-31 18:02:11 +01:00
KuMiShi
034512db64 Logging Exemple 2026-01-31 17:55:24 +01:00
3d601e0f3c edit readme 2026-01-31 17:08:49 +01:00
704c4db244 edit readme 2026-01-31 17:07:27 +01:00
d0e71734a4 Merge branch 'main' of https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle 2026-01-31 17:06:40 +01:00
b0afdc50f3 Added documentation to README.md 2026-01-31 17:06:22 +01:00
KuMiShi
ae234e3718 Gif test 2026-01-31 17:00:09 +01:00
819c03fb80 removed the resource flags 2026-01-31 16:28:21 +01:00
KuMiShi
6160823160 API correction & mcp validation 2026-01-31 15:53:32 +01:00
def1a4be3e deal damage now returns the damage amount dealt
change repercuted in server.py attack tool
2026-01-30 18:40:21 +01:00
62fea062a4 changed the mcp import to match the venv structure 2026-01-30 18:24:56 +01:00
5fef91e110 added some tools to server, removed deprecated
code and adjusted entity, game, and event files to
work with items being mandatory for entities.
2026-01-30 18:14:05 +01:00
KuMiShi
13488e1c2d Merging from api 2026-01-30 17:08:55 +01:00
f16388dda4 removed inventory and item modules and moved item
to utils. Updated imports accordingly.
decentralized entity stats-item coordination.
2026-01-29 22:23:32 +01:00
fc5076d054 commit entity-modify-player-inventory 2026-01-29 22:05:56 +01:00
7b5c411c8d added setters and getters for entity attributes 2026-01-29 21:37:44 +01:00
609485f734 modified imports 2026-01-29 21:17:05 +01:00
12 changed files with 615 additions and 159 deletions

100
README.md
View File

@@ -1,15 +1,18 @@
# Wyvern&Castle
## Sujet et tache
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`).
Le projet contient un serveur (`server.py`) qui intéragit avec notre API de jeu (`game.py`) via un système d'outils (MCP tooling) asynchrone afin de pouvoir enchainer les outils sans trop de délais d'interruption pour les appels long.
## Installation des dépendances
Pour lancer une partie de notre jeu, il faudra installer **l'utilitaire python `UV`** qui est similaire à `pip` ainsi qu'une **application qui contient un LLM** de bureau.
## 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**
Nous vous conseillons d'installer **[Claude Desktop](https://claude.com/fr-fr/download)** (disponible sur Windows
et Mac) qui va servir de LLM de base. Nous n'avons pas essayé pour ce projet mais il serait aussi possible d'utiliser la **version Desktop de ChatGPT**.
1. Installer l'utilitaire python UV :
**1. Installation de UV :**
```bash
# Mac/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -17,27 +20,30 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
2. Initialiser le dossier pour l'installation
**2. Initialiser le dossier pour l'installation**
Creer un dossier et cloner le projet :
```bash
mkdir wyvern_castle
cd wyvern_castle
git clone "https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle.git"
```
3. Creation de l'environnement virtuel
**3. Creation de l'environnement virtuel**
```bash
# Cela est nécessaire afin de pouvoir télécharger les librairies du projet
uv venv
```
Mac/Linux
```bash
# Activation du venv
source .venv/bin/activate
```
Windows
```Powershell
# Activation du venv
.\.venv\Scripts\activate
```
4. Installation des dependances/requirements
**4. Installation des dependances/requirements python**
```bash
# Synchronise l'environnement virtuel du dossier avec les dependances du projet
uv pip sync pyproject.toml
@@ -48,9 +54,13 @@ uv pip compile --upgrade pyproject.toml -o uv.lock
uv pip sync uv.lock
```
5. Changement de la config de Claude Desktop
Modifier le fichier `claude_desktop_config.json`
**5. Configuration de Claude Desktop**
Il faut d'abord se rendre dans les **Paramètres > Développeur** en cliquant sur l'icône du profil en bas à gauche de la fenêtre.
<div align="center">
<img src="images/parameter_claude.PNG"/>
</div>
Une fois dans la partie dev, il faut rajouter la configuration suivante en modifier le fichier `claude_desktop_config.json` par le json ci-dessous:
```json
{
"mcpServers": {
@@ -66,6 +76,74 @@ Modifier le fichier `claude_desktop_config.json`
}
}
```
Si la configuration a bien été effectuée, vous devriez voir le tag `running` à côté du modèle. Autrement, il faudra sûrement **redémarrer l'application**. Si jamais ca ne fonctionne toujours pas, vous pouvez
aller **regarder les logs** ("journaux") pour diagnostiquer l'erreur.
## 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.
<div align="center">
<img src="images/create_player.gif"/>
</div>
Une fois les configurations terminées et que notre modèle tourne bien en local, vous pouvez vous rendre dans **Discussion** et verifier dans l'icône **(+) -> Connecteurs** que votre modèle wyvern_castle est bien coché:
<div align="center">
<img src="images/connector.png"/>
</div>
Si cela est bien coché, alors il ne vous reste plus qu'à écrire votre prompt et laisser l'IA utilise nos outils MCP:
<div align="center">
<img src="images/game_party_state.gif"/>
</div>
## Documentation
### Tools / Outils
Outils liés à la gestion de la partie :
```python
# Charge une sauvegarde
async def load_game(slot:int)
# Sauvegarde une partie
async def save_game(slot:int)
# Récupère le contexte de la partie pour se remettre à jour
async def get_game_state()
```
Routines de création de contenu :
```python
# Crée un joueur
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)
# Crée un personnage non-joueur
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)
# Crée un objet
async def create_item(name: str, description: str, stat_modifier: dict[str,int])
# Crée un événement (fonctionne comme un contexte de combat, d'exploration, etc.)
async def start_event(location:str, initial_description:str, entity_list:list[str])
```
Routines d'actions du joueur et du système
```python
# Lance une attaque
async def perform_attack(src_entity_id:str, target_entity_id:str, attack_type:Stat)
# Lance un test (ex: crocheter une serrure, soulever un poids)
async def perform_simple_action(entity_id:str, stat:Stat, difficulty:int, roll:int, description:str)
# Modifie une statistique (se faire maudire, boire une potion)
async def perform_stat_modification(entity_id:str, stat:Stat, value:int=0)
# Donne un objet à une entité
async def equip_item_to_entity(entity_id: str, item_id: str)
```
Routines utilitaires :
```python
# Récupère les propriétés d'un objet
async def get_item_properties(item_id: str) -> Dict[str, Any]
# Récupère les propriétés d'une entité
async def get_entity_status(entity_id: str) -> Dict[str, Any]
# Récupère l'événement en cours
async def get_current_event() -> Dict[str, Any]
# Récupère la liste de toutes les entités actives
async def get_all_entities_status()
# Lance un dé
async def throw_a_dice(n_faces: int) -> Any
# Lance une pièce
async def toss_coin()
```

View File

@@ -1,4 +1,4 @@
# Game imports
# Game imports
from utils.serializable import Serializable
from utils.item import Item
@@ -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
@@ -14,7 +14,7 @@ class Entity(Serializable):
self.intelligence = 20
self.wisdom = 20
self.charisma = 20
self.hp = 100
self.hp = 20
self.armor = 100
self.speed = 100
@@ -65,7 +65,7 @@ class Entity(Serializable):
def get_hp(self):
if self.equipped_item and "hp" in self.equipped_item.stat_modifier.keys():
return self.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 100)
return self.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 40)
return self.clamp(self.hp, 0, 100)
def get_armor(self):
@@ -98,7 +98,7 @@ class Entity(Serializable):
self.charisma = self.clamp(value, 0, 20)
def set_hp(self, value:int):
self.hp = self.clamp(value, 0, 100)
self.hp = self.clamp(value, 0, 40)
def set_armor(self, value:int):
self.armor = self.clamp(value, 0, 100)
@@ -111,4 +111,4 @@ class Entity(Serializable):
def deal_damage(self, dmg_amount:int):
current_hp = self.get_hp()
self.set_hp(current_hp - dmg_amount)
self.set_hp(current_hp - dmg_amount)

View File

@@ -4,4 +4,3 @@ 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)

View File

@@ -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
@@ -21,6 +21,10 @@ class Event(Serializable):
def remove_entity(self, entity_id:str):
if entity_id in self.entities:
self.entities.remove(entity_id)
def add_entity(self, entity_id:str):
if not entity_id in self.entities:
self.entities.append(entity_id)
def get_current_turn(self):
idx = len(self.turns) - 1
@@ -35,7 +39,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

182
exemple_log.txt Normal file
View File

@@ -0,0 +1,182 @@
2026-01-31T15:39:07.828Z [wyvern_castle] [info] Initializing server... { metadata: undefined }
2026-01-31T15:39:07.845Z [wyvern_castle] [info] Using MCP server command: C:\Users\cleme\AppData\Local\Microsoft\WinGet\Packages\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\uv.exe with args and path: {
metadata: {
args: [
'--directory',
'C:\\Users\\cleme\\Desktop\\Projets\\Wyvern-Castle',
'run',
'server.py',
[length]: 4
],
paths: [
'C:\\Program Files\\Git\\cmd',
'C:\\Program Files\\Git\\mingw64\\bin',
'C:\\Windows\\system32',
'C:\\Windows',
'C:\\Windows\\System32\\Wbem',
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\',
'C:\\Windows\\System32\\OpenSSH\\',
'C:\\Program Files\\dotnet\\',
'C:\\Users\\cleme\\AppData\\Local\\Microsoft\\WindowsApps',
'C:\\Users\\cleme\\AppData\\Local\\Programs\\Microsoft VS Code\\bin',
'C:\\Users\\cleme\\AppData\\Local\\Microsoft\\WinGet\\Packages\\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe',
'',
[length]: 12
]
}
} %o
2026-01-31T15:39:07.970Z [wyvern_castle] [info] Server started and connected successfully { metadata: undefined }
2026-01-31T15:39:07.976Z [wyvern_castle] [info] Message from client: {"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0} { metadata: undefined }
2026-01-31T15:39:08.600Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"wyvern-castle","version":"1.26.0"}}} { metadata: undefined }
2026-01-31T15:39:08.600Z [wyvern_castle] [info] Message from client: {"method":"notifications/initialized","jsonrpc":"2.0"} { metadata: undefined }
2026-01-31T15:39:08.601Z [wyvern_castle] [info] Message from client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":1} { metadata: undefined }
2026-01-31T15:39:08.602Z [wyvern_castle] [info] Message from client: {"method":"prompts/list","params":{},"jsonrpc":"2.0","id":2} { metadata: undefined }
2026-01-31T15:39:08.602Z [wyvern_castle] [info] Message from client: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":3} { metadata: undefined }
[01/31/26 16:39:08] INFO Processing request of type server.py:720
ListToolsRequest
INFO Processing request of type server.py:720
ListPromptsRequest
2026-01-31T15:39:08.609Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"load_game","description":"Loads an already existing game.\n \n Args:\n slot: Integer id of the save slot.\n ","inputSchema":{"properties":{"slot":{"title":"Slot","type":"integer"}},"requir...[9385 chars truncated]...equired":["entity_id"],"title":"get_entity_statusArguments","type":"object"},"outputSchema":{"properties":{"result":{"additionalProperties":true,"title":"Result","type":"object"}},"required":["result"],"title":"get_entity_statusOutput","type":"object"}}]}} { metadata: undefined }
INFO Processing request of type server.py:720
ListResourcesRequest
2026-01-31T15:39:08.610Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":2,"result":{"prompts":[]}} { metadata: undefined }
2026-01-31T15:39:08.611Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":3,"result":{"resources":[]}} { metadata: undefined }
2026-01-31T15:39:59.505Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":4} { metadata: undefined }
[01/31/26 16:39:59] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T15:39:59.509Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 2\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:13.088Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":5} { metadata: undefined }
[01/31/26 16:40:13] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T15:40:13.092Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 15\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:15.720Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":6} { metadata: undefined }
[01/31/26 16:40:15] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T15:40:15.723Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":6,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 11\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:18.803Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":7} { metadata: undefined }
[01/31/26 16:40:18] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T15:40:18.805Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 11\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:21.990Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":8} { metadata: undefined }
[01/31/26 16:40:21] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T15:40:21.993Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 8\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:25.740Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":50}},"jsonrpc":"2.0","id":9} { metadata: undefined }
[01/31/26 16:40:25] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 50 faces server.py:285
2026-01-31T15:40:25.743Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":9,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 7\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:27.834Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":50}},"jsonrpc":"2.0","id":10} { metadata: undefined }
[01/31/26 16:40:27] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 50 faces server.py:285
2026-01-31T15:40:27.837Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":10,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 12\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:31.966Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":50}},"jsonrpc":"2.0","id":11} { metadata: undefined }
[01/31/26 16:40:31] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 50 faces server.py:285
2026-01-31T15:40:31.969Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":11,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 14\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:40.980Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"create_item","arguments":{"name":"Traveler's Walking Stick","description":"A simple but sturdy oak walking stick worn from many journeys","stat_modifier":{"strength":1,"dexterity":1}}},"jsonrpc":"2.0","id":12} { metadata: undefined }
[01/31/26 16:40:40] INFO Processing request of type server.py:720
CallToolRequest
INFO Creating item, name=Traveler's server.py:220
Walking Stick ; description=A simple
but sturdy oak walking stick worn
from many journeys
2026-01-31T15:40:40.983Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":12,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"item_properties\": \"{\\n \\\"id\\\": \\\"f6dcb52d-65ec-48e3-8ed2-a2d47f0206a6\\\",\\n \\\"name\\\": \\\"Traveler's Walking Stick\\\",\\n \\\"description\\\": \\\"A simple but sturdy oak walking stick worn from many journeys\\\",\\n \\\"stat_modifier\\\": {\\n \\\"strength\\\": 1,\\n \\\"dexterity\\\": 1\\n }\\n}\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T15:40:46.587Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"create_player","arguments":{"name":"Clément","strength":2,"dexterity":15,"intelligence":11,"wisdom":11,"charisma":8,"hp":7,"armor":12,"speed":14,"item_id":"f6dcb52d-65ec-48e3-8ed2-a2d47f0206a6"}},"jsonrpc":"2.0","id":13} { metadata: undefined }
[01/31/26 16:40:46] INFO Processing request of type server.py:720
CallToolRequest
INFO Creating player named Clément server.py:323
INFO Creation of player successful server.py:337
2026-01-31T15:40:46.591Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":13,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"player_properties\": \"{\\n \\\"id\\\": \\\"7d280192-e7b3-4374-a31b-d4dd2cd21fd9\\\",\\n \\\"name\\\": \\\"Clément\\\",\\n \\\"strength\\\": 2,\\n \\\"dexterity\\\": 15,\\n \\\"intelligence\\\": 11,\\n \\\"wisdom\\\": 11,\\n \\\"charisma\\\": 8,\\n \\\"hp\\\": 57,\\n \\\"armor\\\": 62,\\n \\\"speed\\\": 64,\\n \\\"equipped_item\\\": {\\n \\\"id\\\": \\\"f6dcb52d-65ec-48e3-8ed2-a2d47f0206a6\\\",\\n \\\"name\\\": \\\"Traveler's Walking Stick\\\",\\n \\\"description\\\": \\\"A simple but sturdy oak walking stick worn from many journeys\\\",\\n \\\"stat_modifier\\\": {\\n \\\"strength\\\": 1,\\n \\\"dexterity\\\": 1\\n }\\n }\\n}\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:32.368Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"start_event","arguments":{"location":"Stream beside Wyvern Castle","initial_description":"Clément sprints towards the sound of running water, his footsteps quick and light. As he reaches the stream's edge, he calls out loudly, his voice echoing off the ancient stone walls.","entity_list":["7d280192-e7b3-4374-a31b-d4dd2cd21fd9"]}},"jsonrpc":"2.0","id":14} { metadata: undefined }
[01/31/26 17:11:32] INFO Processing request of type server.py:720
CallToolRequest
2026-01-31T16:11:32.371Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":14,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"new_event\": \"{\\n \\\"id\\\": \\\"74d04581-878c-4a3c-9ab5-1a322c482bb2\\\",\\n \\\"location\\\": \\\"Stream beside Wyvern Castle\\\",\\n \\\"initial_description\\\": \\\"Clément sprints towards the sound of running water, his footsteps quick and light. As he reaches the stream's edge, he calls out loudly, his voice echoing off the ancient stone walls.\\\",\\n \\\"entities\\\": [\\n \\\"7d280192-e7b3-4374-a31b-d4dd2cd21fd9\\\"\\n ],\\n \\\"turns\\\": [\\n {\\n \\\"actions\\\": {}\\n }\\n ]\\n}\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:39.438Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"toss_coin","arguments":{}},"jsonrpc":"2.0","id":15} { metadata: undefined }
[01/31/26 17:11:39] INFO Processing request of type server.py:720
CallToolRequest
2026-01-31T16:11:39.441Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":15,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"toss_result\": \"head\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:41.828Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":16} { metadata: undefined }
[01/31/26 17:11:41] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:11:41.830Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":16,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 15\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:44.592Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":17} { metadata: undefined }
[01/31/26 17:11:44] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:11:44.594Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":17,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 14\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:47.177Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":18} { metadata: undefined }
[01/31/26 17:11:47] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:11:47.180Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":18,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 7\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:51.688Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":19} { metadata: undefined }
[01/31/26 17:11:51] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:11:51.691Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":19,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 20\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:55.602Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":20} { metadata: undefined }
[01/31/26 17:11:55] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:11:55.605Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":20,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 10\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:11:57.569Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":100}},"jsonrpc":"2.0","id":21} { metadata: undefined }
[01/31/26 17:11:57] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 100 faces server.py:285
2026-01-31T16:11:57.572Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":21,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 63\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:12:01.930Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":100}},"jsonrpc":"2.0","id":22} { metadata: undefined }
[01/31/26 17:12:01] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 100 faces server.py:285
2026-01-31T16:12:01.933Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":22,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 16\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:12:06.208Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":100}},"jsonrpc":"2.0","id":23} { metadata: undefined }
[01/31/26 17:12:06] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 100 faces server.py:285
2026-01-31T16:12:06.211Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":23,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 84\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:12:09.022Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"create_item","arguments":{"description":"A battered wooden shield with faded holy symbols carved into its surface","name":"Guardian's Shield","stat_modifier":{"wisdom":2,"armor":5}}},"jsonrpc":"2.0","id":24} { metadata: undefined }
[01/31/26 17:12:09] INFO Processing request of type server.py:720
CallToolRequest
INFO Creating item, name=Guardian's server.py:220
Shield ; description=A battered
wooden shield with faded holy
symbols carved into its surface
2026-01-31T16:12:09.025Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":24,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"item_properties\": \"{\\n \\\"id\\\": \\\"759f170b-7bf5-46a8-b199-a1e224bff932\\\",\\n \\\"name\\\": \\\"Guardian's Shield\\\",\\n \\\"description\\\": \\\"A battered wooden shield with faded holy symbols carved into its surface\\\",\\n \\\"stat_modifier\\\": {\\n \\\"wisdom\\\": 2,\\n \\\"armor\\\": 5\\n }\\n}\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:12:18.906Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"create_npc","arguments":{"armor":16,"charisma":10,"dexterity":14,"hp":63,"intelligence":7,"item_id":"759f170b-7bf5-46a8-b199-a1e224bff932","name":"Brother Aldric","speed":84,"strength":15,"wisdom":20}},"jsonrpc":"2.0","id":25} { metadata: undefined }
[01/31/26 17:12:18] INFO Processing request of type server.py:720
CallToolRequest
INFO Creating NPC named Brother Aldric server.py:366
INFO Creation of NPC successful server.py:372
2026-01-31T16:12:18.909Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":25,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"npc_properties\": \"{\\n \\\"id\\\": \\\"8476d1a2-260e-4c96-be46-ea642ecfdcba\\\",\\n \\\"name\\\": \\\"Brother Aldric\\\",\\n \\\"strength\\\": 15,\\n \\\"dexterity\\\": 14,\\n \\\"intelligence\\\": 7,\\n \\\"wisdom\\\": 20,\\n \\\"charisma\\\": 10,\\n \\\"hp\\\": 100,\\n \\\"armor\\\": 66,\\n \\\"speed\\\": 100,\\n \\\"equipped_item\\\": {\\n \\\"id\\\": \\\"759f170b-7bf5-46a8-b199-a1e224bff932\\\",\\n \\\"name\\\": \\\"Guardian's Shield\\\",\\n \\\"description\\\": \\\"A battered wooden shield with faded holy symbols carved into its surface\\\",\\n \\\"stat_modifier\\\": {\\n \\\"wisdom\\\": 2,\\n \\\"armor\\\": 5\\n }\\n }\\n}\"\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:26:11.923Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"throw_a_dice","arguments":{"n_faces":20}},"jsonrpc":"2.0","id":26} { metadata: undefined }
[01/31/26 17:26:11] INFO Processing request of type server.py:720
CallToolRequest
INFO Throwing a dice with 20 faces server.py:285
2026-01-31T16:26:11.927Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":26,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"roll_result\": 20\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:26:24.804Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"perform_simple_action","arguments":{"description":"Clément attempts to persuade Brother Aldric with his honest words and extended hand, appealing to the former guardian's sense of purpose","difficulty":12,"entity_id":"7d280192-e7b3-4374-a31b-d4dd2cd21fd9","roll":20,"stat":4}},"jsonrpc":"2.0","id":27} { metadata: undefined }
[01/31/26 17:26:24] INFO Processing request of type server.py:720
CallToolRequest
INFO Entity server.py:142
7d280192-e7b3-4374-a31b-d4dd2cd21fd9
is performing a test action on stat
4 with difficulty 12
2026-01-31T16:26:24.808Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":27,"result":{"content":[{"type":"text","text":"{\n \"success\": true,\n \"action_performed\": true,\n \"test_result\": 22.0,\n \"initial_difficulty\": 12\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:32:05.309Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"all_entities_status","arguments":{}},"jsonrpc":"2.0","id":28} { metadata: undefined }
[01/31/26 17:32:05] INFO Processing request of type server.py:720
CallToolRequest
INFO Getting status of all entities server.py:389
2026-01-31T16:32:05.312Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":28,"result":{"content":[{"type":"text","text":"{\n \"players\": [\n \"{\\n \\\"id\\\": \\\"7d280192-e7b3-4374-a31b-d4dd2cd21fd9\\\",\\n \\\"name\\\": \\\"Clément\\\",\\n \\\"strength\\\": 2,\\n \\\"dexterity\\\": 15,\\n \\\"intelligence\\\": 11,\\n \\\"wisdom\\\": 11,\\n \\\"charisma\\\": 8,\\n \\\"hp\\\": 57,\\n \\\"armor\\\": 62,\\n \\\"speed\\\": 64,\\n \\\"equipped_item\\\": {\\n \\\"id\\\": \\\"f6dcb52d-65ec-48e3-8ed2-a2d47f0206a6\\\",\\n \\\"name\\\": \\\"Traveler's Walking Stick\\\",\\n \\\"description\\\": \\\"A simple but sturdy oak walking stick worn from many journeys\\\",\\n \\\"stat_modifier\\\": {\\n \\\"strength\\\": 1,\\n \\\"dexterity\\\": 1\\n }\\n }\\n}\"\n ],\n \"npcs\": [\n \"{\\n \\\"id\\\": \\\"8476d1a2-260e-4c96-be46-ea642ecfdcba\\\",\\n \\\"name\\\": \\\"Brother Aldric\\\",\\n \\\"strength\\\": 15,\\n \\\"dexterity\\\": 14,\\n \\\"intelligence\\\": 7,\\n \\\"wisdom\\\": 20,\\n \\\"charisma\\\": 10,\\n \\\"hp\\\": 100,\\n \\\"armor\\\": 66,\\n \\\"speed\\\": 100,\\n \\\"equipped_item\\\": {\\n \\\"id\\\": \\\"759f170b-7bf5-46a8-b199-a1e224bff932\\\",\\n \\\"name\\\": \\\"Guardian's Shield\\\",\\n \\\"description\\\": \\\"A battered wooden shield with faded holy symbols carved into its surface\\\",\\n \\\"stat_modifier\\\": {\\n \\\"wisdom\\\": 2,\\n \\\"armor\\\": 5\\n }\\n }\\n}\"\n ]\n}"}],"isError":false}} { metadata: undefined }
2026-01-31T16:32:09.460Z [wyvern_castle] [info] Message from client: {"method":"tools/call","params":{"name":"game_state","arguments":{}},"jsonrpc":"2.0","id":29} { metadata: undefined }
[01/31/26 17:32:09] INFO Processing request of type server.py:720
CallToolRequest
INFO Fetching current game state server.py:82
2026-01-31T16:32:09.464Z [wyvern_castle] [info] Message from server: {"jsonrpc":"2.0","id":29,"result":{"content":[{"type":"text","text":"{\n \"active_players\": [\n {\n \"id\": \"7d280192-e7b3-4374-a31b-d4dd2cd21fd9\",\n \"name\": \"Clément\",\n \"strength\": 2,\n \"dexterity\": 15,\n \"intelli...[4180 chars truncated]...e, (Test difficulty: 12, Player roll: 22.0)\"\n }\n },\n {\n \"actions\": {}\n }\n ]\n }\n ],\n \"turn_order\": [\n \"7d280192-e7b3-4374-a31b-d4dd2cd21fd9\"\n ],\n \"turn_idx\": 0\n}"},"isError":false}} { metadata: undefined }

BIN
images/connector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
images/create_player.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
images/game_party_state.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

BIN
images/parameter_claude.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

406
server.py
View File

@@ -1,24 +1,24 @@
# Game imports
from dice import Dice
from game import Game
from serializable import Serializable
from events.turn import TurnAction
from utils.dice import Dice
from utils.game import Game, Stat
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 mcp.server import FastMCP
import json
import os
# Constants
HISTORY_FILE = "game_history.json"
SAVE_PATH = "save_"
SAVE_PATH = "game_"
# Global Parameters
mcp = FastMCP("wyvern-castle")
game: Game = None
game = Game()
# Logging config
logging.basicConfig(
@@ -29,7 +29,7 @@ logging.basicConfig(
]
)
# SAVING & LOADING GAME STATE
# GLOBAL GAME TOOLS & RESOURCES
@mcp.tool()
async def load_game(slot:int):
"""Loads an already existing game.
@@ -47,6 +47,7 @@ async def load_game(slot:int):
"msg": f"{path} as been successfully loaded!"
}
except OSError as e:
logging.info(f"OSError: " + str(e))
return {
"success": False,
"error": str(e)
@@ -69,83 +70,243 @@ async def save_game(slot:int):
"msg": f"{path} as been successfully saved!"
}
except OSError as e:
logging.info(f"OSError: " + str(e))
return {
"success": False,
"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)
@mcp.tool(name="game_state", description="Retrieves the current game state")
async def get_game_state() -> str:
"""Get the current game state as a serialized string."""
logging.info("Fetching current game state")
return game.serialize()
@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
# EVENTS TOOLS & RESOURCES
@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)
return {
"success": True,
"new_event": new_event.serialize()
}
@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
"""
try:
logging.info(f"Entity {src_entity_id} is performing an attack on {target_entity_id} using {attack_type}")
damage_amount = 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}",
"damage_amount": damage_amount
}
except IndexError as e:
logging.info(f"IndexError: " + str(e))
return {
"success": False,
"error": str(e)
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def perform_simple_action(entity_id:str, stat:Stat, difficulty:int, roll:int, description:str):
"""Perform a simple 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 like Stat.INTELLIGENCE or Stat.DEXTERITY)
difficulty: The difficulty of the test according to a d20
roll: The value of the d20 launched for the test
description: The description of the action being performed by the entity (like opening a door)
"""
try:
logging.info(f"Entity {entity_id} is performing a test action on stat {stat} with difficulty {difficulty}")
action_performed, test_result = game.simple_action(src=entity_id, stat=stat, difficulty=difficulty, roll=roll, description=description)
return {
"success": True,
"action_performed": action_performed,
"test_result": test_result,
"initial_difficulty": difficulty
}
except IndexError as e:
logging.info(f"IndexError: " + str(e))
return {
"success": False,
"error": str(e)
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def perform_stat_modification(entity_id:str, stat:Stat, value:int=0):
"""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)
"""
try:
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}"
}
except IndexError as e:
logging.info(f"IndexError: " + str(e))
return {
"success": False,
"error": str(e)
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
@mcp.tool(name="current_event")
async def get_current_event() -> Dict[str, Any]:
"""Get the current event in the game."""
try:
logging.info("Getting current event")
current_event = game.get_current_event()
return {
"success": True,
"current_event": current_event.serialize()
}
except IndexError as e:
logging.info(f"IndexError: " + str(e))
return {
"success": False,
"error": str(e)
}
# ITEM TOOLS
@mcp.tool()
async def create_item():
pass
async def create_item(name: str, description: str, stat_modifier: dict[str,int]):
"""Create a new item.
Args:
name: Name of the item
description: Description of the item
bonus: Bonus or malus of the item ex: {"strength":+1,"hp":-5}
"""
try:
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
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def add_item_to_entity():
pass
async def equip_item_to_entity(entity_id: str, item_id: str) -> Dict[str, Any]:
"""Equip an item to an entity (player or NPC).
# OTHER UTILS
Args:
entity_id: The id of the entity to equip the item to
item_id: The id of the item to equip
"""
try:
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 {
"success": True,
"msg": f"Item #{item_id} equipped to entity #{entity_id}"
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
@mcp.tool(name="item_properties")
async def get_item_properties(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
"""
try:
logging.info(f"Getting info for item {item_id}")
item = game.get_item(item_id)
return {
"success": True,
"item_properties": item.serialize()
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
# OTHER UTILS (Dice & Entities)
@mcp.tool()
async def throw_a_dice(n_faces: int) -> Any:
"""Throw a dice with n faces. If n==2 its a coin toss.
"""Throw a dice with n faces. The number of faces should be greater than one!
Args:
n_faces: Number of faces of the dice
"""
logging.info(f"Throwing a dice with {n_faces} faces")
dice = Dice(n_faces)
if n_faces < 1:
raise ValueError("Number of faces must be at least 1")
elif n_faces == 1:
return 1
elif n_faces == 2:
return dice.head_or_tails()
return {
"success": False,
"error": "Number of faces must be at least 1"
}
else:
return dice.roll()
return {
"success": True,
"roll_result": Dice.roll(n_faces)
}
@mcp.tool()
async def get_entity_status():
pass
async def toss_coin():
"""Throw a coin when you need head or tails for decision making."""
return {
"success": True,
"toss_result": Dice.head_or_tails()
}
@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 create_player(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item_id:str, add_to_event:bool=True):
"""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 the armor and a d100 for speed.
Args:
name: Name of the player
@@ -158,17 +319,33 @@ async def create_player(name: str, strength: int, dexterity: int, intelligence:
armor: Armor class of the player
speed: Speed of the player
item: Item carried by the player
add_to_event: Boolean deciding whether or not to add the entity to the current event
"""
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()
logging.info(f"Creating player named {name}")
try:
item = game.get_item(item_id) # Check if item exists
player_id = game.create_player(name=name, strength=strength, dexterity=dexterity, intelligence=intelligence, wisdom=wisdom, charisma=charisma, hp=20 + hp, armor=50 + armor, speed=speed, equipped_item=item)
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 player successful")
if add_to_event:
game.add_entity_to_event(player_id)
logging.info(f"Player #{player_id} added to current event!")
return {
"success": True,
"player_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, add_to_event:bool=True):
"""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 d50 for the armor and a d100 for speed.
Args:
name: Name of the NPC
@@ -181,71 +358,62 @@ async def create_npc(name: str, strength: int, dexterity: int, intelligence: int
armor: Armor class of the NPC
speed: Speed of the NPC
item: Item carried by the NPC
add_to_event: Boolean deciding whether or not to add the entity to the current event
"""
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()
logging.info(f"Creating NPC named {name}")
try:
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=20 + hp, armor=50 + armor, speed=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")
if add_to_event:
game.add_entity_to_event(npc_id)
logging.info(f"Player #{npc_id} added to current event!")
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]:
"""Create a new item.
@mcp.tool(name="all_entities_status")
async def get_all_entities_status():
"""
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(name="entity_status")
async def get_entity_status(entity_id: str) -> Dict[str, Any]:
"""Get the full information of an entity (player or NPC).
Args:
name: Name of the item
description: Description of the item
bonus: Bonus of the item ex: strength+1,hp+5
entity_id: The id of the entity to get information from
"""
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()
async def add_item_to_player(player_name: str, item_name: str) -> Dict[str, Any]:
"""Add an item to a player's inventory.
Args:
player_name: The name of the player to add the item to
item_name: The name of the item to add
"""
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."
try:
logging.info(f"Getting info for entity {entity_id}")
entity = game.get_entity(entity_id)
return {
"success": True,
"entity_status": entity.serialize()
}
except ReferenceError as e:
logging.info(f"ReferenceError: " + str(e))
return {
"success": False,
"error": str(e)
}
def main():
# Initialize and run the server

View File

@@ -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,9 +42,10 @@ 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
def get_npc(self, npc_id:str):
for npc in self.active_npcs:
@@ -52,9 +53,10 @@ 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):
for item in self.active_items:
@@ -64,6 +66,8 @@ class Game(Serializable):
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]:
@@ -98,6 +102,9 @@ class Game(Serializable):
self.update_turn_order()
self.turn_idx = 0
def add_entity_to_event(self, entity_id:str):
self.get_current_event().add_entity(entity_id=entity_id)
def check_turn_ended(self):
if self.turn_idx == len(self.turn_order)-1:
# Turn end
@@ -158,18 +165,19 @@ 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)
self.turn_idx += 1
if turn_finished:
self.update_turn_order()
return int(dmg_amount)
def modifying_stat(self, src:str, value:int, stat:int, description:str):
if not self.is_turn_coherent(src):
@@ -200,12 +208,31 @@ class Game(Serializable):
if turn_finished:
self.update_turn_order()
def simple_action(self, src:str, description:str):
def simple_action(self, src:str, stat:Stat, difficulty:int, roll: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!")
turn_finished = self.get_current_event().perform_action(TurnAction.BASIC, src, description=description)
src_entity = self.get_entity(src)
stat_boost = 0 # Between 0 and 5
match(stat):
case Stat.STRENGTH:
stat_boost = src_entity.get_strength() / 4
case Stat.INTELLIGENCE:
stat_boost = src_entity.get_intelligence() / 4
case Stat.DEXTERITY:
stat_boost = src_entity.get_dexterity() / 4
case Stat.WISDOM:
stat_boost = src_entity.get_wisdom() / 4
case Stat.CHARISMA:
stat_boost = src_entity.get_charisma() / 4
test_result = roll + stat_boost
action_performed = difficulty <= test_result
additional_info = f", (Test difficulty: {difficulty}, Player roll: {test_result})"
turn_finished = self.get_current_event().perform_action(TurnAction.BASIC, src, description=description+additional_info)
self.turn_idx += 1
if turn_finished:
self.update_turn_order()
#TODO: Add State Summary as Resource?
return action_performed, test_result

View File

@@ -1,5 +1,5 @@
# Game imports
from serializable import Serializable
from utils.serializable import Serializable
# Native imports
import uuid
@@ -14,5 +14,3 @@ class Item(Serializable):
def __str__(self):
return f"{self.name}: {self.description}"