19 Commits

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
KuMiShi
f2f8af3aec Clean-up and README 2026-01-30 16:31:38 +01:00
KuMiShi
351f676611 API merge final 2026-01-30 16:11:47 +01:00
17 changed files with 964 additions and 241 deletions

2
.gitignore vendored
View File

@@ -2,7 +2,7 @@
.venv/
.python-version
__pycache__/
uv.lock
*.lock
# Save/Load files for testing
*.json

116
README.md
View File

@@ -1,13 +1,18 @@
# Wyvern&Castle
Projet de NLP 2025-2026. Modèle MCP de D&D.
## 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.
# Initialisation du jeu
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`) 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.
Pour lancer la partie il faut tout d'abord installer [Claude Desktop](https://claude.com/fr-fr/download) (disponible sur Windows
et Mac).
## 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.
## Installer l'utilitaire python UV :
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. Installation de UV :**
```bash
# Mac/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -15,38 +20,51 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
## Initialiser le dossier pour l'installation
**2. Initialiser le dossier pour l'installation**
Creer un dossier et cloner le projet :
```bash
uv init wyvern-castle
cd wyvern-castle
mkdir wyvern_castle
cd wyvern_castle
git clone "https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle.git"
```
## Creation de l'environnement virtuel
**3. Creation de l'environnement virtuel**
```bash
# 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
```
## Installation du client mcp
**4. Installation des dependances/requirements python**
```bash
uv add mcp[cli] httpx
# Synchronise l'environnement virtuel du dossier avec les dependances du projet
uv pip sync pyproject.toml
# Si cela ne fonctionne pas correctement, vous pouvez le générer un fichier de dependances avec la commande suivante à partir du .toml:
uv pip compile --upgrade pyproject.toml -o uv.lock
# Puis synchroniser à nouveau (avec le nouveau fichier cette fois)
uv pip sync uv.lock
```
## Changement de la config de Claude Desktop
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": {
"weather": {
"wyvern_castle": {
"command": "uv",
"args": [
"--directory",
@@ -58,4 +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
<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,95 +1,114 @@
# Game imports
from serializable import Serializable
# Game imports
from utils.serializable import Serializable
from utils.item import Item
# Native imports
import uuid
class Entity(Serializable):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
def __init__(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item):
self.id = str(uuid.uuid4())
self.name = name
self.strength = strength
self.dexterity = dexterity
self.intelligence = intelligence
self.wisdom = wisdom
self.charisma = charisma
self.hp = hp
self.armor = armor
self.speed = speed
self.strength = 20
self.dexterity = 20
self.intelligence = 20
self.wisdom = 20
self.charisma = 20
self.hp = 20
self.armor = 100
self.speed = 100
self.set_strength(strength)
self.set_dexterity(dexterity)
self.set_intelligence(intelligence)
self.set_wisdom(wisdom)
self.set_charisma(charisma)
self.set_hp(hp)
self.set_armor(armor)
self.set_speed(speed)
self.equipped_item = None
if equipped_item:
self.set_equipped_item(equipped_item)
def clamp(self, valeur, min_val, max_val):
return max(min_val, min(valeur, max_val))
def get_id(self):
return self.id
def get_strength(self):
if self.equipped_item and "strength" in self.equipped_item.stat_modifier.keys():
return self.strength + self.equipped_item.stat_modifier["strength"]
return self.strength
return self.clamp(self.strength + self.equipped_item.stat_modifier["strength"], 0, 20)
return self.clamp(self.strength, 0, 20)
def get_dexterity(self):
if self.equipped_item and "dexterity" in self.equipped_item.stat_modifier.keys():
return self.dexterity + self.equipped_item.stat_modifier["dexterity"]
return self.dexterity
return self.clamp(self.dexterity + self.equipped_item.stat_modifier["dexterity"], 0, 20)
return self.clamp(self.dexterity, 0, 20)
def get_intelligence(self):
if self.equipped_item and "intelligence" in self.equipped_item.stat_modifier.keys():
return self.intelligence + self.equipped_item.stat_modifier["intelligence"]
return self.intelligence
return self.clamp(self.intelligence + self.equipped_item.stat_modifier["intelligence"], 0, 20)
return self.clamp(self.intelligence, 0, 20)
def get_wisdom(self):
if self.equipped_item and "wisdom" in self.equipped_item.stat_modifier.keys():
return self.wisdom + self.equipped_item.stat_modifier["wisdom"]
return self.wisdom
return self.clamp(self.wisdom + self.equipped_item.stat_modifier["wisdom"], 0, 20)
return self.clamp(self.wisdom, 0, 20)
def get_charisma(self):
if self.equipped_item and "charisma" in self.equipped_item.stat_modifier.keys():
return self.charisma + self.equipped_item.stat_modifier["charisma"]
return self.charisma
return self.clamp(self.charisma + self.equipped_item.stat_modifier["charisma"], 0, 20)
return self.clamp(self.charisma, 0, 20)
def get_hp(self):
if self.equipped_item and "hp" in self.equipped_item.stat_modifier.keys():
return self.hp + self.equipped_item.stat_modifier["hp"]
return self.hp
return self.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 40)
return self.clamp(self.hp, 0, 100)
def get_armor(self):
if self.equipped_item and "armor" in self.equipped_item.stat_modifier.keys():
return self.armor + self.equipped_item.stat_modifier["armor"]
return self.armor
return self.clamp(self.armor + self.equipped_item.stat_modifier["armor"], 0, 100)
return self.clamp(self.armor, 0, 100)
def get_speed(self):
if self.equipped_item and "speed" in self.equipped_item.stat_modifier.keys():
return self.speed + self.equipped_item.stat_modifier["speed"]
return self.speed
return self.clamp(self.speed + self.equipped_item.stat_modifier["speed"], 0, 100)
return self.clamp(self.speed, 0, 100)
def get_equipped_item(self):
return self.equipped_item
def set_strength(self, value):
self.strength = value
def set_strength(self, value:int):
self.strength = self.clamp(value, 0, 20)
def set_dexterity(self, value):
self.dexterity = value
def set_dexterity(self, value:int):
self.dexterity = self.clamp(value, 0, 20)
def set_intelligence(self, value):
self.intelligence = value
def set_intelligence(self, value:int):
self.intelligence = self.clamp(value, 0, 20)
def set_wisdom(self, value):
self.wisdom = value
def set_wisdom(self, value:int):
self.wisdom = self.clamp(value, 0, 20)
def set_charisma(self, value):
self.charisma = value
def set_charisma(self, value:int):
self.charisma = self.clamp(value, 0, 20)
def set_hp(self, value):
self.hp = value
def set_hp(self, value:int):
self.hp = self.clamp(value, 0, 40)
def set_armor(self, value):
self.armor = value
def set_armor(self, value:int):
self.armor = self.clamp(value, 0, 100)
def set_speed(self, value):
self.speed = value
def set_speed(self, value:int):
self.speed = self.clamp(value, 0, 100)
def set_equipped_item(self, item):
def set_equipped_item(self, item:Item):
self.equipped_item = item
def deal_damage(self, dmg_amount:int):
current_hp = self.get_hp()
self.set_hp(current_hp - dmg_amount)

View File

@@ -1,5 +1,7 @@
from entity import Entity
# Game imports
from entities.entity import Entity
class NPC(Entity):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item = None):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)

View File

@@ -1,7 +1,6 @@
from entity import Entity
# Game imports
from entities.entity import Entity
class Player(Entity):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)

View File

@@ -1,14 +1,46 @@
# Game imports
from serializable import Serializable
from utils.serializable import Serializable
from entities.entity import Entity
from events.turn import Turn, TurnAction
# Native imports
import json
import uuid
class Event(Serializable):
def __init__(self, location:str):
def __init__(self, location:str, initial_description:str, entities:list[str]):
super().__init__()
self.id = str(uuid.uuid4())
self.location = location
self.description = ""
self.id:str = str(uuid.uuid4())
self.location:str = location
self.initial_description:str = initial_description
self.entities:list[str] = entities
self.turns:list[Turn] = []
self.add_turn()
def remove_entity(self, entity_id:str):
if entity_id in self.entities:
self.entities.remove(entity_id)
def 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
if idx < 0:
raise IndexError("There is no turns yet, you should create one!")
return self.turns[idx]
def add_turn(self):
self.turns.append(Turn())
def perform_action(self, action:TurnAction, entity_id:str, description:str):
current_turn = self.get_current_turn()
current_turn.add_action(action_type=action, entity_id=entity_id, description=description)
if current_turn.is_finished(len(self.entities)):
self.add_turn()
return True
return False

View File

@@ -1,14 +1,21 @@
# Game import
from utils.serializable import Serializable
# Native imports
from enum import Enum, IntEnum
from enum import Enum
class Action(Enum):
ATTACK = 'strength' # Physical Battle action
FORCE = 'strength' # Actions that requires physical effort
SPELL = 'intelligence' # Many kind of spell (battle or not)
SCAN = 'wisdom' # Danger in environment or NPC's lies
SPEECH = 'charisma' # To persuade or deceive
AGILE = 'dexterity' # Avoid traps or incoming attacks & spell
class TurnAction(Enum):
DAMAGE = 'deal_damage'
STATS = 'modify_stat'
BASIC = 'basic_action'
class BonusAction(IntEnum):
EQUIP_ITEM = 0
USE_CONSUMMABLE = 1
class Turn(Serializable):
def __init__(self):
super().__init__()
self.actions = {}
def add_action(self, action_type:TurnAction, entity_id:str, description:str):
self.actions[entity_id] = f'[{action_type.value}]: ' + description
def is_finished(self, nb_entities:int):
return len(self.actions.keys()) == nb_entities

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 }

43
game.py
View File

@@ -1,43 +0,0 @@
from serializable import Serializable
from utils.dice import Dice
from events.event import Event
from entities.player import Player
from entities.npc import NPC
from utils.item import Item
class Game(Serializable):
def __init__(self, seed:int=42):
self.active_players:list[Player] = []
self.active_npcs:list[NPC] = []
self.active_items:list[Item] = []
self.events:list[Event] = []
def get_player(self, player_id:str):
for player in self.active_players:
if player.id == player_id:
return player
raise ReferenceError(f"The player #{player_id} doesn't exist!")
def get_npc(self, npc_id:str):
for npc in self.active_npcs:
if npc.id == npc_id:
return npc
raise ReferenceError(f"The npc #{npc_id} doesn't exist!")
def get_item(self, item_id:str):
for item in self.active_items:
if item.id == item_id:
return item
raise ReferenceError(f"The item #{item_id} doesn't exist!")
def get_current_event(self):
idx = len(self.events) - 1
if idx < 0:
raise IndexError("There is no event yet, you should create one!")
return self.events[idx]
def add_event(self, new_event:Event):
self.events.append(new_event)
#TODO: Add State Summary as Resource?

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

422
server.py
View File

@@ -1,18 +1,26 @@
# Game imports
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 utils.dice import Dice
from entities.player import Player
from items.item import Item
from game import Game
from entities.npc import NPC
from serializable import Serializable
from mcp.server import FastMCP
import json
import os
mcp = FastMCP("wyvern-castle")
game: Game = None
# Constants
SAVE_PATH = "game_"
# Global Parameters
mcp = FastMCP("wyvern-castle")
game = Game()
# Logging config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -21,10 +29,7 @@ logging.basicConfig(
]
)
# Constants
HISTORY_FILE = "game_history.json"
SAVE_PATH = "save_"
# GLOBAL GAME TOOLS & RESOURCES
@mcp.tool()
async def load_game(slot:int):
"""Loads an already existing game.
@@ -42,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)
@@ -64,37 +70,214 @@ 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)
}
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()
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 & 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_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(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 equip_item_to_entity(entity_id: str, item_id: str) -> Dict[str, Any]:
"""Equip an item to an entity (player or NPC).
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
@@ -102,20 +285,28 @@ async def throw_a_dice(n_faces: int) -> Any:
logging.info(f"Throwing a dice with {n_faces} faces")
if n_faces < 1:
raise ValueError("Number of faces must be at least 1")
elif n_faces == 1:
return 1
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(n_faces)
return {
"success": True,
"roll_result": Dice.roll(n_faces)
}
@mcp.tool()
async def create_player(name: str, strength: int, dexterity: int, intelligence: int, wisdom: int, charisma: int, hp: int, armor: int, speed: int, item: str = "") -> Dict[str, Any]:
async def 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_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
@@ -128,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.active_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
@@ -151,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.active_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.active_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

238
utils/game.py Normal file
View File

@@ -0,0 +1,238 @@
# Game imports
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
from utils.item import Item
from events.turn import TurnAction
# Native imports
from enum import IntEnum
class Stat(IntEnum):
STRENGTH = 0
INTELLIGENCE = 1
DEXTERITY = 2
WISDOM = 3
CHARISMA = 4
HP = 5
ARMOR = 6
SPEED = 7
class Game(Serializable):
def __init__(self, seed:int=42):
self.active_players:list[Player] = []
self.active_npcs:list[NPC] = []
self.active_items:list[Item] = []
self.events:list[Event] = []
self.turn_order:list[str] = []
self.turn_idx = 0
def get_entity(self, entity_id:str):
for entity in (self.active_players + self.active_npcs):
if entity.id == entity_id:
return entity
raise ReferenceError(f"The player #{entity_id} doesn't exist!")
def get_player(self, player_id:str):
for player in self.active_players:
if player.id == player_id:
return player
raise ReferenceError(f"The player #{player_id} doesn't exist!")
def create_player(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item):
new_player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)
self.active_players.append(new_player)
return new_player.id
def get_npc(self, npc_id:str):
for npc in self.active_npcs:
if npc.id == npc_id:
return npc
raise ReferenceError(f"The npc #{npc_id} doesn't exist!")
def create_npc(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item):
new_npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)
self.active_npcs.append(new_npc)
return new_npc.id
def get_item(self, item_id:str):
for item in self.active_items:
if item.id == item_id:
return item
raise ReferenceError(f"The item #{item_id} doesn't exist!")
def create_item(self,name:str, description:str, stat_modifier:dict[str, int]):
new_item = Item(name, description, stat_modifier)
self.active_items.append(new_item)
return new_item.id
def add_player(self, new_player:Player):
if new_player.id in [player.id for player in self.active_players]:
raise ReferenceError(f"Player id #{new_player.id} already present in game!")
self.active_players.append(new_player)
def add_ncp(self, new_ncp:NPC):
if new_ncp.id in [npc.id for npc in self.active_npcs]:
raise ReferenceError(f"NCP id #{new_ncp.id} already present in game!")
self.active_npcs.append(new_ncp)
def add_item(self, new_item:Item):
if new_item.id in [item.id for item in self.active_items]:
raise ReferenceError(f"Item id #{new_item.id} already present in game!")
self.active_items.append(new_item)
def add_item_to_entity(self, item_id:str, entity_id:str):
item = self.get_item(item_id)
entity = self.get_entity(entity_id)
entity.set_equipped_item(item)
self.active_items.remove(item)
def get_current_event(self):
idx = len(self.events) - 1
if idx < 0:
raise IndexError("There is no event yet, you should create one!")
return self.events[idx]
def add_event(self, new_event:Event):
self.events.append(new_event)
self.update_turn_order()
self.turn_idx = 0
def 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
self.get_current_event().add_turn()
self.update_turn_order()
def update_turn_order(self):
active_entities = self.get_current_event().entities
entity_list = [self.get_entity(id) for id in active_entities]
entity_list = self.sort_entities_by_speed(entity_list)
self.turn_order = [entity.id for entity in entity_list]
self.turn_idx = 0
# Selection sort based on entity's speed
def sort_entities_by_speed(self, entity_list:list[NPC|Player]):
n = len(entity_list)
for i in range(n):
max_idx = i # Current fastest entity
for j in range(i+1, n):
entity_max = entity_list[max_idx]
entity_j = entity_list[j]
if entity_max.speed < entity_j.speed:
max_idx = j # New Maximum Speed for j entity
# Swapping current index i with the new maximum in i+1, n-1
entity_list[i], entity_list[max_idx] = entity_list[max_idx], entity_list[i]
return entity_list
def kill_entity(self, entity_id:str):
dead_entity = self.get_entity(entity_id)
self.get_current_event().remove_entity(entity_id=entity_id)
if isinstance(dead_entity, NPC):
self.active_npcs.remove(dead_entity)
if isinstance(dead_entity, Player):
self.active_players.remove(dead_entity)
def is_turn_coherent(self, entity_id:str):
return self.turn_order[self.turn_idx] == entity_id
def deal_damage(self, src:str, target:str, roll:int, stat:int, description:str):
if not self.is_turn_coherent(src):
raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!")
src_entity = self.get_entity(src)
target_entity = self.get_entity(target)
dmg_amount = 0
if stat == Stat.STRENGTH: # Strength damages physical and long closed range weapons
dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_strength()
elif stat == Stat.INTELLIGENCE: # Using magic
dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_intelligence()
elif stat == Stat.DEXTERITY: # Using daggers, bows or throws
dmg_amount += (roll * 5 / target_entity.get_armor()) * src_entity.get_dexterity()
target_entity.deal_damage(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.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):
raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!")
src_entity = self.get_entity(src)
match(stat):
case Stat.STRENGTH:
src_entity.set_strength(src_entity.strength + value)
case Stat.INTELLIGENCE:
src_entity.set_intelligence(src_entity.intelligence + value)
case Stat.DEXTERITY:
src_entity.set_dexterity(src_entity.dexterity + value)
case Stat.WISDOM:
src_entity.set_wisdom(src_entity.wisdom + value)
case Stat.CHARISMA:
src_entity.set_charisma(src_entity.charisma + value)
case Stat.HP:
src_entity.set_hp(src_entity.hp + value)
case Stat.ARMOR:
src_entity.set_armor(src_entity.armor + value)
case Stat.SPEED:
src_entity.set_speed(src_entity.speed + value)
turn_finished = self.get_current_event().perform_action(TurnAction.STATS, src, description=description)
self.turn_idx += 1
if turn_finished:
self.update_turn_order()
def simple_action(self, src:str, 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!")
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()
return action_performed, test_result

View File

@@ -1,10 +1,11 @@
from typing import Dict
from serializable import Serializable
# Game imports
from utils.serializable import Serializable
# Native imports
import uuid
class Item(Serializable):
def __init__(self,name:str, description:str, stat_modifier:Dict[str, int]):
def __init__(self,name:str, description:str, stat_modifier:dict[str, int]):
super().__init__()
self.id = str(uuid.uuid4())
self.name = name
@@ -13,4 +14,3 @@ class Item(Serializable):
def __str__(self):
return f"{self.name}: {self.description}"

View File

@@ -1,3 +1,4 @@
# Native imports
import json
from typing import Any, Dict, List, Type, TypeVar