4 Commits

Author SHA1 Message Date
f16388dda4 removed inventory and item modules and moved item
to utils. Updated imports accordingly.
decentralized entity stats-item coordination.
2026-01-29 22:23:32 +01:00
fc5076d054 commit entity-modify-player-inventory 2026-01-29 22:05:56 +01:00
7b5c411c8d added setters and getters for entity attributes 2026-01-29 21:37:44 +01:00
609485f734 modified imports 2026-01-29 21:17:05 +01:00
12 changed files with 142 additions and 409 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,15 +1,13 @@
# Wyvern&Castle # Wyvern&Castle
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. Projet de NLP 2025-2026. Modèle MCP de D&D.
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 projet # Initialisation du jeu
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**
1. Installer l'utilitaire python UV : Pour lancer la partie il faut tout d'abord installer [Claude Desktop](https://claude.com/fr-fr/download) (disponible sur Windows
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
@@ -17,14 +15,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"
``` ```
2. Initialiser le dossier pour l'installation ## Initialiser le dossier pour l'installation
Creer un dossier et cloner le projet : Creer un dossier et cloner le projet :
```bash ```bash
mkdir wyvern_castle uv init 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"
``` ```
3. Creation de l'environnement virtuel ## Creation de l'environnement virtuel
```bash ```bash
uv venv uv venv
``` ```
@@ -37,24 +35,18 @@ Windows
.\.venv\Scripts\activate .\.venv\Scripts\activate
``` ```
4. Installation des dependances/requirements ## Installation du client mcp
```bash ```bash
# Synchronise l'environnement virtuel du dossier avec les dependances du projet uv add mcp[cli] httpx
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
``` ```
5. Changement de la config de Claude Desktop ## 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": {
"wyvern_castle": { "weather": {
"command": "uv", "command": "uv",
"args": [ "args": [
"--directory", "--directory",
@@ -67,5 +59,3 @@ 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.

View File

@@ -1,114 +1,95 @@
# Game imports # Game imports
from utils.serializable import Serializable from 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:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item=None): def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
self.id = str(uuid.uuid4()) self.id = str(uuid.uuid4())
self.name = name self.name = name
self.strength = 20 self.strength = strength
self.dexterity = 20 self.dexterity = dexterity
self.intelligence = 20 self.intelligence = intelligence
self.wisdom = 20 self.wisdom = wisdom
self.charisma = 20 self.charisma = charisma
self.hp = 100 self.hp = hp
self.armor = 100 self.armor = armor
self.speed = 100 self.speed = speed
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.clamp(self.strength + self.equipped_item.stat_modifier["strength"], 0, 20) return self.strength + self.equipped_item.stat_modifier["strength"]
return self.clamp(self.strength, 0, 20) return self.strength
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.clamp(self.dexterity + self.equipped_item.stat_modifier["dexterity"], 0, 20) return self.dexterity + self.equipped_item.stat_modifier["dexterity"]
return self.clamp(self.dexterity, 0, 20) return self.dexterity
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.clamp(self.intelligence + self.equipped_item.stat_modifier["intelligence"], 0, 20) return self.intelligence + self.equipped_item.stat_modifier["intelligence"]
return self.clamp(self.intelligence, 0, 20) return self.intelligence
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.clamp(self.wisdom + self.equipped_item.stat_modifier["wisdom"], 0, 20) return self.wisdom + self.equipped_item.stat_modifier["wisdom"]
return self.clamp(self.wisdom, 0, 20) return self.wisdom
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.clamp(self.charisma + self.equipped_item.stat_modifier["charisma"], 0, 20) return self.charisma + self.equipped_item.stat_modifier["charisma"]
return self.clamp(self.charisma, 0, 20) return self.charisma
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.clamp(self.hp + self.equipped_item.stat_modifier["hp"], 0, 100) return self.hp + self.equipped_item.stat_modifier["hp"]
return self.clamp(self.hp, 0, 100) return self.hp
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.clamp(self.armor + self.equipped_item.stat_modifier["armor"], 0, 100) return self.armor + self.equipped_item.stat_modifier["armor"]
return self.clamp(self.armor, 0, 100) return self.armor
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.clamp(self.speed + self.equipped_item.stat_modifier["speed"], 0, 100) return self.speed + self.equipped_item.stat_modifier["speed"]
return self.clamp(self.speed, 0, 100) return self.speed
def get_equipped_item(self): def get_equipped_item(self):
return self.equipped_item return self.equipped_item
def set_strength(self, value:int): def set_strength(self, value):
self.strength = self.clamp(value, 0, 20) self.strength = value
def set_dexterity(self, value:int): def set_dexterity(self, value):
self.dexterity = self.clamp(value, 0, 20) self.dexterity = value
def set_intelligence(self, value:int): def set_intelligence(self, value):
self.intelligence = self.clamp(value, 0, 20) self.intelligence = value
def set_wisdom(self, value:int): def set_wisdom(self, value):
self.wisdom = self.clamp(value, 0, 20) self.wisdom = value
def set_charisma(self, value:int): def set_charisma(self, value):
self.charisma = self.clamp(value, 0, 20) self.charisma = value
def set_hp(self, value:int): def set_hp(self, value):
self.hp = self.clamp(value, 0, 100) self.hp = value
def set_armor(self, value:int): def set_armor(self, value):
self.armor = self.clamp(value, 0, 100) self.armor = value
def set_speed(self, value:int): def set_speed(self, value):
self.speed = self.clamp(value, 0, 100) self.speed = value
def set_equipped_item(self, item:Item): def set_equipped_item(self, 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)

View File

@@ -1,7 +1,5 @@
# Game imports from entity import Entity
from entities.entity import Entity
class NPC(Entity): class NPC(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):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item) super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)

View File

@@ -1,7 +1,7 @@
# Game imports from entity import Entity
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)

View File

@@ -1,42 +1,14 @@
# Game imports # Game imports
from utils.serializable import Serializable from 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, initial_description:str, entities:list[str]): def __init__(self, location:str):
super().__init__() super().__init__()
self.id:str = str(uuid.uuid4()) self.id = str(uuid.uuid4())
self.location:str = location self.location = location
self.initial_description:str = initial_description self.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

View File

@@ -1,21 +1,14 @@
# Game import
from utils.serializable import Serializable
# Native imports # Native imports
from enum import Enum from enum import Enum, IntEnum
class TurnAction(Enum): class Action(Enum):
DAMAGE = 'deal_damage' ATTACK = 'strength' # Physical Battle action
STATS = 'modify_stat' FORCE = 'strength' # Actions that requires physical effort
BASIC = 'basic_action' SPELL = 'intelligence' # Many kind of spell (battle or not)
SCAN = 'wisdom' # Danger in environment or NPC's lies
SPEECH = 'charisma' # To persuade or deceive
AGILE = 'dexterity' # Avoid traps or incoming attacks & spell
class Turn(Serializable): class BonusAction(IntEnum):
def __init__(self): EQUIP_ITEM = 0
super().__init__() USE_CONSUMMABLE = 1
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 Normal file
View File

@@ -0,0 +1,43 @@
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?

View File

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

View File

@@ -1,26 +1,18 @@
# 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',
@@ -29,7 +21,10 @@ logging.basicConfig(
] ]
) )
# SAVING & LOADING GAME STATE # Constants
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.
@@ -74,7 +69,6 @@ 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 = []
@@ -88,7 +82,6 @@ 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):
@@ -99,26 +92,6 @@ 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.
@@ -127,20 +100,17 @@ 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() return Dice.roll(n_faces)
@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]:
@@ -162,7 +132,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.players.append(player) game.active_players.append(player)
return player.serialize_dict() return player.serialize_dict()
@mcp.tool() @mcp.tool()
@@ -185,7 +155,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.npcs.append(npc) game.active_npcs.append(npc)
return npc.serialize_dict() return npc.serialize_dict()
@mcp.tool() @mcp.tool()
@@ -200,7 +170,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.items.append(item) game.active_items.append(item)
return item.serialize_dict() return item.serialize_dict()
@mcp.tool() @mcp.tool()

View File

@@ -1,211 +0,0 @@
# 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?

View File

@@ -1,11 +1,10 @@
# Game imports from typing import Dict
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
@@ -15,4 +14,3 @@ class Item(Serializable):
def __str__(self): def __str__(self):
return f"{self.name}: {self.description}" return f"{self.name}: {self.description}"