Server & Game refactor basics

This commit is contained in:
KuMiShi
2026-01-29 16:55:45 +01:00
parent 50d2b16224
commit e655de8f54
23 changed files with 189 additions and 121 deletions

2
.gitignore vendored
View File

@@ -3,4 +3,6 @@
.python-version
__pycache__/
uv.lock
# Save/Load files for testing
*.json

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
dice.py
View File

@@ -1,13 +1,13 @@
# Native imports
import random as rd
class Dice():
def __init__(self, num_faces:int=20):
self.num_faces = num_faces
@staticmethod
def roll(num_faces=20):
return rd.randrange(start=1, stop=num_faces+1, step=1)
def roll(self):
return rd.randrange(start=1, stop=self.num_faces+1, step=1)
def head_or_tails(self):
@staticmethod
def head_or_tails():
result = rd.randint(0,1)
if result: # true
return "head" # face

22
entities/entity.py Normal file
View File

@@ -0,0 +1,22 @@
# Game imports
from serializable import Serializable
# Native imports
import uuid
class Entity(Serializable):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_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
def get_id(self):
return self.id

View File

@@ -1,5 +1,5 @@
from entity import 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):
super().__init__(name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed)

View File

@@ -1,30 +0,0 @@
from serializable import Serializable
from enum import Enum, IntEnum
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 BonusAction(IntEnum):
EQUIP_ITEM = 0
USE_CONSUMMABLE = 1
class Entity(Serializable):
def __init__(self, name, strength, dexterity, intelligence, wisdom, charisma, hp, armor, speed, equipped_item=None):
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
def make_a_turn(self, action:Action, bonus_action:BonusAction):
pass

View File

@@ -1,26 +0,0 @@
from serializable import Serializable
from entity import Action, BonusAction, Entity
import json
class Event(Serializable):
def __init__(self, location:str, entities_objectives:dict[str,str]):
super().__init__()
self.location = location
self.entities_objectives = entities_objectives
self.description = ""
def add_turn_event(self, event_text:str, action:Action, bonus:BonusAction):
event_dict = {}
event_dict['text'] = event_text
event_dict['action'] = action
event_dict['bonus'] = bonus
self.description += json.dumps(event_dict)
def update_objectives(self, entities:list[Entity], objectives:list[str]):
assert len(entities) == len(objectives), f"Number of entities & objectives are different: {len(entities)} (entities) and {len(objectives)} (objectives) !"
n = len(entities)
for i in range(n):
entity_name = entities[i].name
objective = objectives[i]
self.entities_objectives[entity_name] = objective

14
events/event.py Normal file
View File

@@ -0,0 +1,14 @@
# Game imports
from serializable import Serializable
from entities.entity import Entity
# Native imports
import json
import uuid
class Event(Serializable):
def __init__(self, location:str):
super().__init__()
self.id = str(uuid.uuid4())
self.location = location
self.description = ""

14
events/turn.py Normal file
View File

@@ -0,0 +1,14 @@
# Native imports
from enum import Enum, IntEnum
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 BonusAction(IntEnum):
EQUIP_ITEM = 0
USE_CONSUMMABLE = 1

41
game.py
View File

@@ -1,10 +1,43 @@
from serializable import Serializable
from dice import Dice
from events.event import Event
from entities.player import Player
from entities.npc import NPC
from items.item import Item
class Game(Serializable):
def __init__(self, seed:int=42):
self.players = []
self.npcs = []
self.items = []
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,39 +0,0 @@
from serializable import Serializable
from item import Item
class Inventory(Serializable):
def __init__(self, max_capacity:float = 50.0):
super().__init__()
self.items:list[Item] = []
self.max_capacity = max_capacity # Weight (kg)
def current_capacity(self):
return sum([item.weight for item in 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 added_item.weight + self.current_capacity() > self.max_capacity:
return f'{added_item.name} is too heavy to fit inside the inventory: {added_item.weight + self.current_capacity() - self.max_capacity}kg in surplus!'
else:
self.items.append(added_item)
return f'{added_item.name} added to inventory. Current items load: {self.current_capacity()}kg'
def remove_item(self, r_item):
if r_item in self.items:
self.items.remove(r_item)
return f'{r_item.name} was removed from the inventory'
else:
return f'{r_item.name} is not present within the inventory'
def get_item(self, item_name:str):
for item in self.items:
if item.name == item_name:
return item
# The item was not found
return None

37
items/inventory.py Normal file
View File

@@ -0,0 +1,37 @@
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,19 +1,21 @@
from serializable import Serializable
import uuid
class Item(Serializable):
def __init__(self,name:str, description:str, stat_modifier:str, weight:float = 10.0):
def __init__(self,name:str, description:str, stat_modifier:str):
super().__init__()
self.id = str(uuid.uuid4())
self.name = name
self.description = description
self.stat_modifier = stat_modifier
self.weight = weight
def __str__(self):
return f"{self.name} ({self.weight} kg): {self.description}"
return f"{self.name}: {self.description}"
class Equippable(Item):
def __init__(self, name, description, stat_modifier, equipped:bool, weight = 10.0):
super().__init__(name, description, stat_modifier, weight)
def __init__(self, name, description, stat_modifier, equipped:bool):
super().__init__(name, description, stat_modifier)
self.equipped = equipped
def equip(self):
@@ -23,8 +25,8 @@ class Equippable(Item):
self.equipped = False
class Consummable(Item):
def __init__(self, name, description, stat_modifier, nb_of_uses:int, weight = 10.0):
super().__init__(name, description, stat_modifier, weight)
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):

View File

@@ -1,6 +0,0 @@
def main():
print("Hello from mcp-nlp!")
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from serializable import Serializable
import json
import os
mcp = FastMCP("wyvern-castle")
game = Game()
game: Game = None
logging.basicConfig(
level=logging.INFO,
@@ -21,8 +21,53 @@ logging.basicConfig(
]
)
# History file path
# Constants
HISTORY_FILE = "game_history.json"
SAVE_PATH = "save_"
@mcp.tool()
async def load_game(slot:int):
"""Loads an already existing game.
Args:
slot: Integer id of the save slot.
"""
global game
path = SAVE_PATH + str(slot) + ".json"
try:
with open(path, "r", encoding="utf-8") as f:
game.deserialize(f.read())
return {
"success": True,
"msg": f"{path} as been successfully loaded!"
}
except OSError as e:
return {
"success": False,
"error": str(e)
}
@mcp.tool()
async def save_game(slot:int):
"""Saves the current game to the given slot.
Args:
slot: Integer id of the save slot.
"""
global game
path = SAVE_PATH + str(slot) + ".json"
try:
with open(path, "w", encoding="utf-8") as f:
f.write(game.serialize())
return {
"success": True,
"msg": f"{path} as been successfully saved!"
}
except OSError as e:
return {
"success": False,
"error": str(e)
}
def append_to_history(event: Dict[str, Any]):
"""Append a game event to the history file."""