API merge final

This commit is contained in:
KuMiShi
2026-01-30 16:11:47 +01:00
parent e655de8f54
commit 351f676611
9 changed files with 370 additions and 105 deletions

View File

@@ -1,22 +1,114 @@
# Game imports
from serializable import Serializable
from items.item import Item
# Native imports
import uuid
class Entity(Serializable):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
def __init__(self, name:str, strength:int, dexterity:int, intelligence:int, wisdom:int, charisma:int, hp:int, armor:int, speed:int, equipped_item:Item=None):
self.id = str(uuid.uuid4())
self.name = name
self.strength = strength
self.dexterity = dexterity
self.intelligence = intelligence
self.wisdom = wisdom
self.charisma = charisma
self.hp = hp
self.armor = armor
self.speed = speed
self.equipped_item = equipped_item
self.strength = 20
self.dexterity = 20
self.intelligence = 20
self.wisdom = 20
self.charisma = 20
self.hp = 100
self.armor = 100
self.speed = 100
self.set_strength(strength)
self.set_dexterity(dexterity)
self.set_intelligence(intelligence)
self.set_wisdom(wisdom)
self.set_charisma(charisma)
self.set_hp(hp)
self.set_armor(armor)
self.set_speed(speed)
self.equipped_item = None
if equipped_item:
self.set_equipped_item(equipped_item)
def clamp(self, valeur, min_val, max_val):
return max(min_val, min(valeur, max_val))
def get_id(self):
return self.id
return self.id
def get_strength(self):
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.clamp(self.strength, 0, 20)
def get_dexterity(self):
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.clamp(self.dexterity, 0, 20)
def get_intelligence(self):
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.clamp(self.intelligence, 0, 20)
def get_wisdom(self):
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.clamp(self.wisdom, 0, 20)
def get_charisma(self):
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.clamp(self.charisma, 0, 20)
def get_hp(self):
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.clamp(self.hp, 0, 100)
def get_armor(self):
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.clamp(self.armor, 0, 100)
def get_speed(self):
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.clamp(self.speed, 0, 100)
def get_equipped_item(self):
return self.equipped_item
def set_strength(self, value:int):
self.strength = self.clamp(value, 0, 20)
def set_dexterity(self, value:int):
self.dexterity = self.clamp(value, 0, 20)
def set_intelligence(self, value:int):
self.intelligence = self.clamp(value, 0, 20)
def set_wisdom(self, value:int):
self.wisdom = self.clamp(value, 0, 20)
def set_charisma(self, value:int):
self.charisma = self.clamp(value, 0, 20)
def set_hp(self, value:int):
self.hp = self.clamp(value, 0, 100)
def set_armor(self, value:int):
self.armor = self.clamp(value, 0, 100)
def set_speed(self, value:int):
self.speed = self.clamp(value, 0, 100)
def set_equipped_item(self, item:Item):
self.equipped_item = item
def deal_damage(self, dmg_amount:int):
current_hp = self.get_hp()
self.set_hp(current_hp - dmg_amount)

View File

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

View File

@@ -1,9 +1,6 @@
from inventory import Inventory
from entity import Entity
from entities.entity import Entity
class Player(Entity):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, item=None):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)
self.inventory = Inventory()
if item:
self.inventory.add_item(item)
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item)

View File

@@ -1,14 +1,42 @@
# Game imports
from serializable import Serializable
from entities.entity import Entity
from turn import Turn, TurnAction
# Native imports
import json
import uuid
class Event(Serializable):
def __init__(self, location:str):
def __init__(self, location:str, initial_description:str, entities:list[str]):
super().__init__()
self.id = str(uuid.uuid4())
self.location = location
self.description = ""
self.id:str = str(uuid.uuid4())
self.location:str = location
self.initial_description:str = initial_description
self.entities:list[str] = entities
self.turns:list[Turn] = []
self.add_turn()
def remove_entity(self, entity_id:str):
if entity_id in self.entities:
self.entities.remove(entity_id)
def 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,14 +1,21 @@
# Game import
from serializable import Serializable
# Native imports
from enum import Enum, IntEnum
from enum import Enum
class Action(Enum):
ATTACK = 'strength' # Physical Battle action
FORCE = 'strength' # Actions that requires physical effort
SPELL = 'intelligence' # Many kind of spell (battle or not)
SCAN = 'wisdom' # Danger in environment or NPC's lies
SPEECH = 'charisma' # To persuade or deceive
AGILE = 'dexterity' # Avoid traps or incoming attacks & spell
class TurnAction(Enum):
DAMAGE = 'deal_damage'
STATS = 'modify_stat'
BASIC = 'basic_action'
class BonusAction(IntEnum):
EQUIP_ITEM = 0
USE_CONSUMMABLE = 1
class Turn(Serializable):
def __init__(self):
super().__init__()
self.actions = {}
def add_action(self, action_type:TurnAction, entity_id:str, description:str):
self.actions[entity_id] = f'[{action_type.value}]: ' + description
def is_finished(self, nb_entities:int):
return len(self.actions.keys()) == nb_entities

166
game.py
View File

@@ -4,6 +4,19 @@ from events.event import Event
from entities.player import Player
from entities.npc import NPC
from items.item import Item
from events.turn import TurnAction
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):
@@ -12,24 +25,65 @@ class Game(Serializable):
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
@@ -39,5 +93,117 @@ class Game(Serializable):
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,37 +0,0 @@
from serializable import Serializable
from items.item import Item
class Inventory(Serializable):
def __init__(self, max_capacity:int = 5):
super().__init__()
self.items:list[Item] = []
self.max_capacity = max_capacity # Maximum umber of items
def current_capacity(self):
return len(self.items)
def list_items(self):
s_items = ''
for item in self.items:
s_items += item.__str__() + '; '
return s_items
def add_item(self, added_item:Item):
if self.current_capacity() == self.max_capacity:
return f'The inventory is full!'
else:
self.items.append(added_item)
return f'{added_item.name} added to inventory. Current number of items: {self.current_capacity()}/{self.max_capacity}'
def remove_item(self, item_id:str):
searched_item = self.get_item(item_id=item_id)
if searched_item:
return f'{searched_item.name} was removed from the inventory.'
raise ValueError(f'Item #{item_id} is not present within the inventory!')
def get_item(self, item_id:str):
for item in self.items:
if item.id == item_id:
return item
# The item was not found
return None

View File

@@ -1,9 +1,10 @@
from typing import Dict
from serializable import Serializable
import uuid
class Item(Serializable):
def __init__(self,name:str, description:str, stat_modifier:str):
def __init__(self,name:str, description:str, stat_modifier:Dict[str, int]):
super().__init__()
self.id = str(uuid.uuid4())
self.name = name
@@ -13,23 +14,4 @@ class Item(Serializable):
def __str__(self):
return f"{self.name}: {self.description}"
class Equippable(Item):
def __init__(self, name, description, stat_modifier, equipped:bool):
super().__init__(name, description, stat_modifier)
self.equipped = equipped
def equip(self):
self.equipped = True
def unequip(self):
self.equipped = False
class Consummable(Item):
def __init__(self, name, description, stat_modifier, nb_of_uses:int):
super().__init__(name, description, stat_modifier)
self.nb_of_uses = nb_of_uses
def consumme(self):
if self.nb_of_uses > 0:
self.nb_of_uses -= 1

View File

@@ -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
import logging
import httpx
from mcp.server.fastmcp import FastMCP
from dice import Dice
from player import Player
from item import Item
from game import Game
from npc import NPC
from serializable import Serializable
import json
import os
# Constants
HISTORY_FILE = "game_history.json"
SAVE_PATH = "save_"
# Global Parameters
mcp = FastMCP("wyvern-castle")
game: Game = None
# Logging config
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -21,10 +29,7 @@ logging.basicConfig(
]
)
# Constants
HISTORY_FILE = "game_history.json"
SAVE_PATH = "save_"
# SAVING & LOADING GAME STATE
@mcp.tool()
async def load_game(slot:int):
"""Loads an already existing game.
@@ -69,6 +74,7 @@ async def save_game(slot:int):
"error": str(e)
}
@DeprecationWarning
def append_to_history(event: Dict[str, Any]):
"""Append a game event to the history file."""
history = []
@@ -82,6 +88,7 @@ def append_to_history(event: Dict[str, Any]):
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)
@DeprecationWarning
def read_history() -> list:
"""Read the game history from the file."""
if os.path.exists(HISTORY_FILE):
@@ -92,6 +99,26 @@ def read_history() -> list:
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()
async def throw_a_dice(n_faces: int) -> Any:
"""Throw a dice with n faces. If n==2 its a coin toss.
@@ -111,7 +138,9 @@ async def throw_a_dice(n_faces: int) -> Any:
else:
return dice.roll()
@mcp.tool()
async def get_entity_status():
pass
@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]: