Compare commits
19 Commits
julien-wor
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b1146a892 | ||
|
|
74f0df49bd | ||
| a6391f961d | |||
| 05f58cdbf5 | |||
| e129815248 | |||
|
|
034512db64 | ||
| 3d601e0f3c | |||
| 704c4db244 | |||
| d0e71734a4 | |||
| b0afdc50f3 | |||
|
|
ae234e3718 | ||
| 819c03fb80 | |||
|
|
6160823160 | ||
| def1a4be3e | |||
| 62fea062a4 | |||
| 5fef91e110 | |||
|
|
13488e1c2d | ||
|
|
f2f8af3aec | ||
|
|
351f676611 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,7 +2,7 @@
|
||||
.venv/
|
||||
.python-version
|
||||
__pycache__/
|
||||
uv.lock
|
||||
*.lock
|
||||
|
||||
# Save/Load files for testing
|
||||
*.json
|
||||
116
README.md
116
README.md
@@ -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()
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
182
exemple_log.txt
Normal 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
43
game.py
@@ -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
BIN
images/connector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
images/create_player.gif
Normal file
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
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
BIN
images/parameter_claude.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
422
server.py
422
server.py
@@ -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
238
utils/game.py
Normal 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
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Native imports
|
||||
import json
|
||||
from typing import Any, Dict, List, Type, TypeVar
|
||||
|
||||
Reference in New Issue
Block a user