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/
|
.venv/
|
||||||
.python-version
|
.python-version
|
||||||
__pycache__/
|
__pycache__/
|
||||||
uv.lock
|
*.lock
|
||||||
|
|
||||||
# Save/Load files for testing
|
# Save/Load files for testing
|
||||||
*.json
|
*.json
|
||||||
116
README.md
116
README.md
@@ -1,13 +1,18 @@
|
|||||||
# Wyvern&Castle
|
# 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
|
## Installation des dépendances
|
||||||
et Mac).
|
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
|
```bash
|
||||||
# Mac/Linux
|
# Mac/Linux
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
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"
|
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 :
|
Creer un dossier et cloner le projet :
|
||||||
```bash
|
```bash
|
||||||
uv init wyvern-castle
|
mkdir wyvern_castle
|
||||||
cd wyvern-castle
|
cd wyvern_castle
|
||||||
git clone "https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle.git"
|
git clone "https://gitea.galaxynoliro.fr/KuMiShi/Wyvern-Castle.git"
|
||||||
```
|
```
|
||||||
## Creation de l'environnement virtuel
|
**3. Creation de l'environnement virtuel**
|
||||||
```bash
|
```bash
|
||||||
|
# Cela est nécessaire afin de pouvoir télécharger les librairies du projet
|
||||||
uv venv
|
uv venv
|
||||||
```
|
```
|
||||||
Mac/Linux
|
Mac/Linux
|
||||||
```bash
|
```bash
|
||||||
|
# Activation du venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
Windows
|
Windows
|
||||||
```Powershell
|
```Powershell
|
||||||
|
# Activation du venv
|
||||||
.\.venv\Scripts\activate
|
.\.venv\Scripts\activate
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation du client mcp
|
**4. Installation des dependances/requirements python**
|
||||||
```bash
|
```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
|
**5. Configuration de Claude Desktop**
|
||||||
Modifier le fichier `claude_desktop_config.json`
|
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
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"weather": {
|
"wyvern_castle": {
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": [
|
"args": [
|
||||||
"--directory",
|
"--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
|
# Game imports
|
||||||
from serializable import Serializable
|
from utils.serializable import Serializable
|
||||||
|
from utils.item import Item
|
||||||
|
|
||||||
# Native imports
|
# Native imports
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class Entity(Serializable):
|
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.id = str(uuid.uuid4())
|
||||||
self.name = name
|
self.name = name
|
||||||
self.strength = strength
|
self.strength = 20
|
||||||
self.dexterity = dexterity
|
self.dexterity = 20
|
||||||
self.intelligence = intelligence
|
self.intelligence = 20
|
||||||
self.wisdom = wisdom
|
self.wisdom = 20
|
||||||
self.charisma = charisma
|
self.charisma = 20
|
||||||
self.hp = hp
|
self.hp = 20
|
||||||
self.armor = armor
|
self.armor = 100
|
||||||
self.speed = speed
|
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
|
self.equipped_item = None
|
||||||
|
|
||||||
if equipped_item:
|
if equipped_item:
|
||||||
self.set_equipped_item(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):
|
def get_id(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
def get_strength(self):
|
def get_strength(self):
|
||||||
if self.equipped_item and "strength" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "strength" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.strength + self.equipped_item.stat_modifier["strength"]
|
return self.clamp(self.strength + self.equipped_item.stat_modifier["strength"], 0, 20)
|
||||||
return self.strength
|
return self.clamp(self.strength, 0, 20)
|
||||||
|
|
||||||
def get_dexterity(self):
|
def get_dexterity(self):
|
||||||
if self.equipped_item and "dexterity" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "dexterity" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.dexterity + self.equipped_item.stat_modifier["dexterity"]
|
return self.clamp(self.dexterity + self.equipped_item.stat_modifier["dexterity"], 0, 20)
|
||||||
return self.dexterity
|
return self.clamp(self.dexterity, 0, 20)
|
||||||
|
|
||||||
def get_intelligence(self):
|
def get_intelligence(self):
|
||||||
if self.equipped_item and "intelligence" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "intelligence" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.intelligence + self.equipped_item.stat_modifier["intelligence"]
|
return self.clamp(self.intelligence + self.equipped_item.stat_modifier["intelligence"], 0, 20)
|
||||||
return self.intelligence
|
return self.clamp(self.intelligence, 0, 20)
|
||||||
|
|
||||||
def get_wisdom(self):
|
def get_wisdom(self):
|
||||||
if self.equipped_item and "wisdom" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "wisdom" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.wisdom + self.equipped_item.stat_modifier["wisdom"]
|
return self.clamp(self.wisdom + self.equipped_item.stat_modifier["wisdom"], 0, 20)
|
||||||
return self.wisdom
|
return self.clamp(self.wisdom, 0, 20)
|
||||||
|
|
||||||
def get_charisma(self):
|
def get_charisma(self):
|
||||||
if self.equipped_item and "charisma" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "charisma" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.charisma + self.equipped_item.stat_modifier["charisma"]
|
return self.clamp(self.charisma + self.equipped_item.stat_modifier["charisma"], 0, 20)
|
||||||
return self.charisma
|
return self.clamp(self.charisma, 0, 20)
|
||||||
|
|
||||||
def get_hp(self):
|
def get_hp(self):
|
||||||
if self.equipped_item and "hp" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "hp" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.hp + self.equipped_item.stat_modifier["hp"]
|
return self.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 40)
|
||||||
return self.hp
|
return self.clamp(self.hp, 0, 100)
|
||||||
|
|
||||||
def get_armor(self):
|
def get_armor(self):
|
||||||
if self.equipped_item and "armor" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "armor" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.armor + self.equipped_item.stat_modifier["armor"]
|
return self.clamp(self.armor + self.equipped_item.stat_modifier["armor"], 0, 100)
|
||||||
return self.armor
|
return self.clamp(self.armor, 0, 100)
|
||||||
|
|
||||||
def get_speed(self):
|
def get_speed(self):
|
||||||
if self.equipped_item and "speed" in self.equipped_item.stat_modifier.keys():
|
if self.equipped_item and "speed" in self.equipped_item.stat_modifier.keys():
|
||||||
return self.speed + self.equipped_item.stat_modifier["speed"]
|
return self.clamp(self.speed + self.equipped_item.stat_modifier["speed"], 0, 100)
|
||||||
return self.speed
|
return self.clamp(self.speed, 0, 100)
|
||||||
|
|
||||||
|
|
||||||
def get_equipped_item(self):
|
def get_equipped_item(self):
|
||||||
return self.equipped_item
|
return self.equipped_item
|
||||||
|
|
||||||
def set_strength(self, value):
|
def set_strength(self, value:int):
|
||||||
self.strength = value
|
self.strength = self.clamp(value, 0, 20)
|
||||||
|
|
||||||
def set_dexterity(self, value):
|
def set_dexterity(self, value:int):
|
||||||
self.dexterity = value
|
self.dexterity = self.clamp(value, 0, 20)
|
||||||
|
|
||||||
def set_intelligence(self, value):
|
def set_intelligence(self, value:int):
|
||||||
self.intelligence = value
|
self.intelligence = self.clamp(value, 0, 20)
|
||||||
|
|
||||||
def set_wisdom(self, value):
|
def set_wisdom(self, value:int):
|
||||||
self.wisdom = value
|
self.wisdom = self.clamp(value, 0, 20)
|
||||||
|
|
||||||
def set_charisma(self, value):
|
def set_charisma(self, value:int):
|
||||||
self.charisma = value
|
self.charisma = self.clamp(value, 0, 20)
|
||||||
|
|
||||||
def set_hp(self, value):
|
def set_hp(self, value:int):
|
||||||
self.hp = value
|
self.hp = self.clamp(value, 0, 40)
|
||||||
|
|
||||||
def set_armor(self, value):
|
def set_armor(self, value:int):
|
||||||
self.armor = value
|
self.armor = self.clamp(value, 0, 100)
|
||||||
|
|
||||||
def set_speed(self, value):
|
def set_speed(self, value:int):
|
||||||
self.speed = value
|
self.speed = self.clamp(value, 0, 100)
|
||||||
|
|
||||||
def set_equipped_item(self, item):
|
def set_equipped_item(self, item:Item):
|
||||||
self.equipped_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):
|
class NPC(Entity):
|
||||||
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed):
|
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item = None):
|
||||||
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
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):
|
class Player(Entity):
|
||||||
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
|
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)
|
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,14 +1,46 @@
|
|||||||
# Game imports
|
# Game imports
|
||||||
from serializable import Serializable
|
from utils.serializable import Serializable
|
||||||
from entities.entity import Entity
|
from entities.entity import Entity
|
||||||
|
from events.turn import Turn, TurnAction
|
||||||
|
|
||||||
# Native imports
|
# Native imports
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class Event(Serializable):
|
class Event(Serializable):
|
||||||
def __init__(self, location:str):
|
def __init__(self, location:str, initial_description:str, entities:list[str]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.id = str(uuid.uuid4())
|
self.id:str = str(uuid.uuid4())
|
||||||
self.location = location
|
self.location:str = location
|
||||||
self.description = ""
|
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
|
# Native imports
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum
|
||||||
|
|
||||||
class Action(Enum):
|
class TurnAction(Enum):
|
||||||
ATTACK = 'strength' # Physical Battle action
|
DAMAGE = 'deal_damage'
|
||||||
FORCE = 'strength' # Actions that requires physical effort
|
STATS = 'modify_stat'
|
||||||
SPELL = 'intelligence' # Many kind of spell (battle or not)
|
BASIC = 'basic_action'
|
||||||
SCAN = 'wisdom' # Danger in environment or NPC's lies
|
|
||||||
SPEECH = 'charisma' # To persuade or deceive
|
|
||||||
AGILE = 'dexterity' # Avoid traps or incoming attacks & spell
|
|
||||||
|
|
||||||
class BonusAction(IntEnum):
|
class Turn(Serializable):
|
||||||
EQUIP_ITEM = 0
|
def __init__(self):
|
||||||
USE_CONSUMMABLE = 1
|
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
|
from typing import Any, Dict
|
||||||
import logging
|
import logging
|
||||||
import httpx
|
import httpx
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server 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
|
|
||||||
import json
|
import json
|
||||||
import os
|
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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
@@ -21,10 +29,7 @@ logging.basicConfig(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Constants
|
# GLOBAL GAME TOOLS & RESOURCES
|
||||||
HISTORY_FILE = "game_history.json"
|
|
||||||
SAVE_PATH = "save_"
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def load_game(slot:int):
|
async def load_game(slot:int):
|
||||||
"""Loads an already existing game.
|
"""Loads an already existing game.
|
||||||
@@ -42,6 +47,7 @@ async def load_game(slot:int):
|
|||||||
"msg": f"{path} as been successfully loaded!"
|
"msg": f"{path} as been successfully loaded!"
|
||||||
}
|
}
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
logging.info(f"OSError: " + str(e))
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
@@ -64,37 +70,214 @@ async def save_game(slot:int):
|
|||||||
"msg": f"{path} as been successfully saved!"
|
"msg": f"{path} as been successfully saved!"
|
||||||
}
|
}
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
logging.info(f"OSError: " + str(e))
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
def append_to_history(event: Dict[str, Any]):
|
@mcp.tool(name="game_state", description="Retrieves the current game state")
|
||||||
"""Append a game event to the history file."""
|
async def get_game_state() -> str:
|
||||||
history = []
|
"""Get the current game state as a serialized string."""
|
||||||
if os.path.exists(HISTORY_FILE):
|
logging.info("Fetching current game state")
|
||||||
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
return game.serialize()
|
||||||
try:
|
|
||||||
history = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
history = []
|
|
||||||
history.append(event)
|
|
||||||
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def read_history() -> list:
|
# EVENTS TOOLS & RESOURCES
|
||||||
"""Read the game history from the file."""
|
@mcp.tool()
|
||||||
if os.path.exists(HISTORY_FILE):
|
async def start_event(location:str, initial_description:str, entity_list:list[str]):
|
||||||
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
""" Start a new event in the game.
|
||||||
try:
|
Args:
|
||||||
return json.load(f)
|
location: Location of the event
|
||||||
except json.JSONDecodeError:
|
initial_description: Initial description of the event
|
||||||
return []
|
entity_list: List of entity IDs involved in the event
|
||||||
return []
|
"""
|
||||||
|
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()
|
@mcp.tool()
|
||||||
async def throw_a_dice(n_faces: int) -> Any:
|
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:
|
Args:
|
||||||
n_faces: Number of faces of the dice
|
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")
|
logging.info(f"Throwing a dice with {n_faces} faces")
|
||||||
|
|
||||||
if n_faces < 1:
|
if n_faces < 1:
|
||||||
raise ValueError("Number of faces must be at least 1")
|
return {
|
||||||
elif n_faces == 1:
|
"success": False,
|
||||||
return 1
|
"error": "Number of faces must be at least 1"
|
||||||
elif n_faces == 2:
|
}
|
||||||
return Dice.head_or_tails()
|
|
||||||
else:
|
else:
|
||||||
return Dice.roll(n_faces)
|
return {
|
||||||
|
"success": True,
|
||||||
|
"roll_result": Dice.roll(n_faces)
|
||||||
|
}
|
||||||
|
|
||||||
@mcp.tool()
|
@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,
|
"""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:
|
Args:
|
||||||
name: Name of the player
|
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
|
armor: Armor class of the player
|
||||||
speed: Speed of the player
|
speed: Speed of the player
|
||||||
item: Item carried by 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}")
|
logging.info(f"Creating player named {name}")
|
||||||
player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
try:
|
||||||
logging.info(f"Created player: {player}")
|
item = game.get_item(item_id) # Check if item exists
|
||||||
game.active_players.append(player)
|
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)
|
||||||
return player.serialize_dict()
|
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()
|
@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,
|
"""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:
|
Args:
|
||||||
name: Name of the NPC
|
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
|
armor: Armor class of the NPC
|
||||||
speed: Speed of the NPC
|
speed: Speed of the NPC
|
||||||
item: Item carried by 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}")
|
logging.info(f"Creating NPC named {name}")
|
||||||
npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
try:
|
||||||
logging.info(f"Created NPC: {npc}")
|
item = game.get_item(item_id) # Check if item exists
|
||||||
game.active_npcs.append(npc)
|
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)
|
||||||
return npc.serialize_dict()
|
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()
|
@mcp.tool(name="all_entities_status")
|
||||||
async def create_item(name: str, description: str, bonus: str) -> Dict[str, Any]:
|
async def get_all_entities_status():
|
||||||
"""Create a new item.
|
"""
|
||||||
|
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:
|
Args:
|
||||||
name: Name of the item
|
entity_id: The id of the entity to get information from
|
||||||
description: Description of the item
|
|
||||||
bonus: Bonus of the item ex: strength+1,hp+5
|
|
||||||
"""
|
"""
|
||||||
logging.info(f"Creating item with name={name}")
|
try:
|
||||||
item = Item(name, description, bonus)
|
logging.info(f"Getting info for entity {entity_id}")
|
||||||
logging.info(f"Created item: {item}")
|
entity = game.get_entity(entity_id)
|
||||||
game.active_items.append(item)
|
return {
|
||||||
return item.serialize_dict()
|
"success": True,
|
||||||
|
"entity_status": entity.serialize()
|
||||||
@mcp.tool()
|
}
|
||||||
async def add_item_to_player(player_name: str, item_name: str) -> Dict[str, Any]:
|
except ReferenceError as e:
|
||||||
"""Add an item to a player's inventory.
|
logging.info(f"ReferenceError: " + str(e))
|
||||||
|
return {
|
||||||
Args:
|
"success": False,
|
||||||
player_name: The name of the player to add the item to
|
"error": str(e)
|
||||||
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."
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Initialize and run the server
|
# 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
|
# Game imports
|
||||||
from serializable import Serializable
|
from utils.serializable import Serializable
|
||||||
|
|
||||||
|
# Native imports
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
class Item(Serializable):
|
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__()
|
super().__init__()
|
||||||
self.id = str(uuid.uuid4())
|
self.id = str(uuid.uuid4())
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -13,4 +14,3 @@ class Item(Serializable):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}: {self.description}"
|
return f"{self.name}: {self.description}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# Native imports
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, Type, TypeVar
|
from typing import Any, Dict, List, Type, TypeVar
|
||||||
|
|
||||||
Reference in New Issue
Block a user