Compare commits
2 Commits
julien-wor
...
api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
38
README.md
38
README.md
@@ -1,13 +1,15 @@
|
|||||||
# Wyvern&Castle
|
# Wyvern&Castle
|
||||||
Projet de NLP 2025-2026. Modèle MCP de D&D.
|
Ceci est un projet de **modèle MCP** ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) pour le cours de **Natural Language Programming** de l'année 2025-2026.
|
||||||
|
|
||||||
|
L'objectif du projet a été de concevoir une **version simplifié du jeu Donjon & Dragon** avec un LLM capable de générer des parties et des scénarios qui n'ont de limite que votre imagination (et celles du LLM aussi!).
|
||||||
|
Le projet contient un serveur (`server.py`) qui intéragit avec notre API de jeu (`game.py`).
|
||||||
|
|
||||||
|
|
||||||
# Initialisation du jeu
|
## Initialisation du projet
|
||||||
|
Pour lancer une partie de notre jeu, nous vous conseillons d'installer **[Claude Desktop](https://claude.com/fr-fr/download)** (disponible sur Windows
|
||||||
|
et Mac) qui va servir de LLM. Nous n'avons pas essayé mais il serait aussi possible d'utiliser la **version Desktop de ChatGPT**
|
||||||
|
|
||||||
Pour lancer la partie il faut tout d'abord installer [Claude Desktop](https://claude.com/fr-fr/download) (disponible sur Windows
|
1. Installer l'utilitaire python UV :
|
||||||
et Mac).
|
|
||||||
|
|
||||||
## Installer l'utilitaire python 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,14 +17,14 @@ 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
|
||||||
uv venv
|
uv venv
|
||||||
```
|
```
|
||||||
@@ -35,18 +37,24 @@ Windows
|
|||||||
.\.venv\Scripts\activate
|
.\.venv\Scripts\activate
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation du client mcp
|
4. Installation des dependances/requirements
|
||||||
```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. Changement de la config de Claude Desktop
|
||||||
Modifier le fichier `claude_desktop_config.json`
|
Modifier le fichier `claude_desktop_config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"weather": {
|
"wyvern_castle": {
|
||||||
"command": "uv",
|
"command": "uv",
|
||||||
"args": [
|
"args": [
|
||||||
"--directory",
|
"--directory",
|
||||||
@@ -59,3 +67,5 @@ Modifier le fichier `claude_desktop_config.json`
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
Le projet est assez simple d'utilisation car une fois le serveur lancé, il vous suffit d'écrire des prompts à l'aide de votre application Desktop de LLM. Il est aussi possible d'avoir accès à une aide de génération de prompt intégrée.
|
||||||
|
|||||||
@@ -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=None):
|
||||||
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 = 100
|
||||||
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, 100)
|
||||||
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, 100)
|
||||||
|
|
||||||
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,7 @@
|
|||||||
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,42 @@
|
|||||||
# Game imports
|
# Game imports
|
||||||
from serializable import Serializable
|
from utils.serializable import Serializable
|
||||||
from entities.entity import Entity
|
from entities.entity import Entity
|
||||||
|
from 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 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():
|
||||||
|
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
|
||||||
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?
|
|
||||||
62
server.py
62
server.py
@@ -1,18 +1,26 @@
|
|||||||
|
# Game imports
|
||||||
|
from dice import Dice
|
||||||
|
from game import Game
|
||||||
|
from 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.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
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
HISTORY_FILE = "game_history.json"
|
||||||
|
SAVE_PATH = "save_"
|
||||||
|
|
||||||
|
# Global Parameters
|
||||||
mcp = FastMCP("wyvern-castle")
|
mcp = FastMCP("wyvern-castle")
|
||||||
game: Game = None
|
game: Game = None
|
||||||
|
|
||||||
|
# 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
|
# SAVING & LOADING GAME STATE
|
||||||
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.
|
||||||
@@ -69,6 +74,7 @@ async def save_game(slot:int):
|
|||||||
"error": str(e)
|
"error": str(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeprecationWarning
|
||||||
def append_to_history(event: Dict[str, Any]):
|
def append_to_history(event: Dict[str, Any]):
|
||||||
"""Append a game event to the history file."""
|
"""Append a game event to the history file."""
|
||||||
history = []
|
history = []
|
||||||
@@ -82,6 +88,7 @@ def append_to_history(event: Dict[str, Any]):
|
|||||||
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
json.dump(history, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
@DeprecationWarning
|
||||||
def read_history() -> list:
|
def read_history() -> list:
|
||||||
"""Read the game history from the file."""
|
"""Read the game history from the file."""
|
||||||
if os.path.exists(HISTORY_FILE):
|
if os.path.exists(HISTORY_FILE):
|
||||||
@@ -92,6 +99,26 @@ def read_history() -> list:
|
|||||||
return []
|
return []
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# EVENTS TOOLS
|
||||||
|
@mcp.tool()
|
||||||
|
async def start_event(location:str, initial_description:str, entity_list:list[str]):
|
||||||
|
new_event = Event(location=location, initial_description=initial_description, entities=entity_list)
|
||||||
|
game.add_event(new_event)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def perform_action():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ITEM TOOLS
|
||||||
|
@mcp.tool()
|
||||||
|
async def create_item():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def add_item_to_entity():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# OTHER UTILS
|
||||||
@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. If n==2 its a coin toss.
|
||||||
@@ -100,17 +127,20 @@ async def throw_a_dice(n_faces: int) -> Any:
|
|||||||
n_faces: Number of faces of the dice
|
n_faces: Number of faces of the dice
|
||||||
"""
|
"""
|
||||||
logging.info(f"Throwing a dice with {n_faces} faces")
|
logging.info(f"Throwing a dice with {n_faces} faces")
|
||||||
|
dice = Dice(n_faces)
|
||||||
|
|
||||||
if n_faces < 1:
|
if n_faces < 1:
|
||||||
raise ValueError("Number of faces must be at least 1")
|
raise ValueError("Number of faces must be at least 1")
|
||||||
elif n_faces == 1:
|
elif n_faces == 1:
|
||||||
return 1
|
return 1
|
||||||
elif n_faces == 2:
|
elif n_faces == 2:
|
||||||
return Dice.head_or_tails()
|
return dice.head_or_tails()
|
||||||
else:
|
else:
|
||||||
return Dice.roll(n_faces)
|
return dice.roll()
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_entity_status():
|
||||||
|
pass
|
||||||
|
|
||||||
@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 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]:
|
||||||
@@ -132,7 +162,7 @@ async def create_player(name: str, strength: int, dexterity: int, intelligence:
|
|||||||
logging.info(f"Creating player with name={name}")
|
logging.info(f"Creating player with name={name}")
|
||||||
player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
||||||
logging.info(f"Created player: {player}")
|
logging.info(f"Created player: {player}")
|
||||||
game.active_players.append(player)
|
game.players.append(player)
|
||||||
return player.serialize_dict()
|
return player.serialize_dict()
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -155,7 +185,7 @@ async def create_npc(name: str, strength: int, dexterity: int, intelligence: int
|
|||||||
logging.info(f"Creating NPC with name={name}")
|
logging.info(f"Creating NPC with name={name}")
|
||||||
npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
|
||||||
logging.info(f"Created NPC: {npc}")
|
logging.info(f"Created NPC: {npc}")
|
||||||
game.active_npcs.append(npc)
|
game.npcs.append(npc)
|
||||||
return npc.serialize_dict()
|
return npc.serialize_dict()
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -170,7 +200,7 @@ async def create_item(name: str, description: str, bonus: str) -> Dict[str, Any]
|
|||||||
logging.info(f"Creating item with name={name}")
|
logging.info(f"Creating item with name={name}")
|
||||||
item = Item(name, description, bonus)
|
item = Item(name, description, bonus)
|
||||||
logging.info(f"Created item: {item}")
|
logging.info(f"Created item: {item}")
|
||||||
game.active_items.append(item)
|
game.items.append(item)
|
||||||
return item.serialize_dict()
|
return item.serialize_dict()
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
|
|||||||
211
utils/game.py
Normal file
211
utils/game.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# Game imports
|
||||||
|
from serializable import Serializable
|
||||||
|
from 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=None):
|
||||||
|
new_player = Player(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)
|
||||||
|
self.active_players.append(new_player)
|
||||||
|
|
||||||
|
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=None):
|
||||||
|
new_npc = NPC(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)
|
||||||
|
self.active_players.append(new_npc)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 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(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)
|
||||||
|
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()
|
||||||
|
|
||||||
|
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, description:str):
|
||||||
|
if not self.is_turn_coherent(src):
|
||||||
|
raise ReferenceError(f"Entity #{src} tried performing an action while it was #{self.turn_order[self.turn_idx]}'s turn!")
|
||||||
|
|
||||||
|
turn_finished = self.get_current_event().perform_action(TurnAction.BASIC, src, description=description)
|
||||||
|
self.turn_idx += 1
|
||||||
|
if turn_finished:
|
||||||
|
self.update_turn_order()
|
||||||
|
#TODO: Add State Summary as Resource?
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from typing import Dict
|
# Game imports
|
||||||
from serializable import Serializable
|
from 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
|
||||||
@@ -14,3 +15,4 @@ 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