303 lines
8.9 KiB
Python
303 lines
8.9 KiB
Python
"""
|
|
Game Mechanics API Blueprint
|
|
|
|
This module provides API endpoints for game mechanics that determine
|
|
outcomes before AI narration:
|
|
- Skill checks (perception, persuasion, stealth, etc.)
|
|
- Search/loot actions
|
|
- Dice rolls
|
|
|
|
These endpoints return structured results that can be used for UI
|
|
dice animations and then passed to AI for narrative description.
|
|
"""
|
|
|
|
from flask import Blueprint, request
|
|
|
|
from app.services.outcome_service import outcome_service
|
|
from app.services.character_service import get_character_service, CharacterNotFound
|
|
from app.game_logic.dice import SkillType, Difficulty
|
|
from app.utils.response import (
|
|
success_response,
|
|
error_response,
|
|
not_found_response,
|
|
validation_error_response
|
|
)
|
|
from app.utils.auth import require_auth, get_current_user
|
|
from app.utils.logging import get_logger
|
|
|
|
|
|
# Initialize logger
|
|
logger = get_logger(__file__)
|
|
|
|
# Create blueprint
|
|
game_mechanics_bp = Blueprint('game_mechanics', __name__, url_prefix='/api/v1/game')
|
|
|
|
|
|
# Valid skill types for API validation
|
|
VALID_SKILL_TYPES = [skill.name.lower() for skill in SkillType]
|
|
|
|
# Valid difficulty names
|
|
VALID_DIFFICULTIES = ["trivial", "easy", "medium", "hard", "very_hard", "nearly_impossible"]
|
|
|
|
|
|
@game_mechanics_bp.route('/check', methods=['POST'])
|
|
@require_auth
|
|
def perform_check():
|
|
"""
|
|
Perform a skill check or search action.
|
|
|
|
This endpoint determines the outcome of chance-based actions before
|
|
they are passed to AI for narration. The result includes all dice
|
|
roll details for UI display.
|
|
|
|
Request JSON:
|
|
{
|
|
"character_id": "...",
|
|
"check_type": "search" | "skill",
|
|
"skill": "perception", // Required for skill checks
|
|
"dc": 15, // Optional, can use difficulty instead
|
|
"difficulty": "medium", // Optional, alternative to dc
|
|
"location_type": "forest", // For search checks
|
|
"context": {} // Optional additional context
|
|
}
|
|
|
|
Returns:
|
|
For search checks:
|
|
{
|
|
"check_result": {
|
|
"roll": 14,
|
|
"modifier": 3,
|
|
"total": 17,
|
|
"dc": 15,
|
|
"success": true,
|
|
"margin": 2
|
|
},
|
|
"items_found": [...],
|
|
"gold_found": 5
|
|
}
|
|
|
|
For skill checks:
|
|
{
|
|
"check_result": {...},
|
|
"context": {
|
|
"skill_used": "persuasion",
|
|
"stat_used": "charisma",
|
|
...
|
|
}
|
|
}
|
|
"""
|
|
user = get_current_user()
|
|
data = request.get_json()
|
|
|
|
if not data:
|
|
return validation_error_response(
|
|
message="Request body is required",
|
|
details={"field": "body", "issue": "Missing JSON body"}
|
|
)
|
|
|
|
# Validate required fields
|
|
character_id = data.get("character_id")
|
|
check_type = data.get("check_type")
|
|
|
|
if not character_id:
|
|
return validation_error_response(
|
|
message="Character ID is required",
|
|
details={"field": "character_id", "issue": "Missing required field"}
|
|
)
|
|
|
|
if not check_type:
|
|
return validation_error_response(
|
|
message="Check type is required",
|
|
details={"field": "check_type", "issue": "Missing required field"}
|
|
)
|
|
|
|
if check_type not in ["search", "skill"]:
|
|
return validation_error_response(
|
|
message="Invalid check type",
|
|
details={"field": "check_type", "issue": "Must be 'search' or 'skill'"}
|
|
)
|
|
|
|
# Get character and verify ownership
|
|
try:
|
|
character_service = get_character_service()
|
|
character = character_service.get_character(character_id)
|
|
|
|
if character.user_id != user["user_id"]:
|
|
return error_response(
|
|
status_code=403,
|
|
message="You don't have permission to access this character",
|
|
error_code="FORBIDDEN"
|
|
)
|
|
except CharacterNotFound:
|
|
return not_found_response(
|
|
message=f"Character not found: {character_id}"
|
|
)
|
|
except Exception as e:
|
|
logger.error("character_fetch_error", error=str(e), character_id=character_id)
|
|
return error_response(
|
|
status_code=500,
|
|
message="Failed to fetch character",
|
|
error_code="CHARACTER_FETCH_ERROR"
|
|
)
|
|
|
|
# Determine DC from difficulty name or direct value
|
|
dc = data.get("dc")
|
|
difficulty = data.get("difficulty")
|
|
|
|
if dc is None and difficulty:
|
|
if difficulty.lower() not in VALID_DIFFICULTIES:
|
|
return validation_error_response(
|
|
message="Invalid difficulty",
|
|
details={
|
|
"field": "difficulty",
|
|
"issue": f"Must be one of: {', '.join(VALID_DIFFICULTIES)}"
|
|
}
|
|
)
|
|
dc = outcome_service.get_dc_for_difficulty(difficulty)
|
|
elif dc is None:
|
|
# Default to medium difficulty
|
|
dc = Difficulty.MEDIUM.value
|
|
|
|
# Validate DC range
|
|
if not isinstance(dc, int) or dc < 1 or dc > 35:
|
|
return validation_error_response(
|
|
message="Invalid DC value",
|
|
details={"field": "dc", "issue": "DC must be an integer between 1 and 35"}
|
|
)
|
|
|
|
# Get optional bonus
|
|
bonus = data.get("bonus", 0)
|
|
if not isinstance(bonus, int):
|
|
bonus = 0
|
|
|
|
# Perform the check based on type
|
|
try:
|
|
if check_type == "search":
|
|
# Search check uses perception
|
|
location_type = data.get("location_type", "default")
|
|
outcome = outcome_service.determine_search_outcome(
|
|
character=character,
|
|
location_type=location_type,
|
|
dc=dc,
|
|
bonus=bonus
|
|
)
|
|
|
|
logger.info(
|
|
"search_check_performed",
|
|
user_id=user["user_id"],
|
|
character_id=character_id,
|
|
location_type=location_type,
|
|
success=outcome.check_result.success
|
|
)
|
|
|
|
return success_response(result=outcome.to_dict())
|
|
|
|
else: # skill check
|
|
skill = data.get("skill")
|
|
if not skill:
|
|
return validation_error_response(
|
|
message="Skill is required for skill checks",
|
|
details={"field": "skill", "issue": "Missing required field"}
|
|
)
|
|
|
|
skill_lower = skill.lower()
|
|
if skill_lower not in VALID_SKILL_TYPES:
|
|
return validation_error_response(
|
|
message="Invalid skill type",
|
|
details={
|
|
"field": "skill",
|
|
"issue": f"Must be one of: {', '.join(VALID_SKILL_TYPES)}"
|
|
}
|
|
)
|
|
|
|
# Convert to SkillType enum
|
|
skill_type = SkillType[skill.upper()]
|
|
|
|
# Get additional context
|
|
context = data.get("context", {})
|
|
|
|
outcome = outcome_service.determine_skill_check_outcome(
|
|
character=character,
|
|
skill_type=skill_type,
|
|
dc=dc,
|
|
bonus=bonus,
|
|
context=context
|
|
)
|
|
|
|
logger.info(
|
|
"skill_check_performed",
|
|
user_id=user["user_id"],
|
|
character_id=character_id,
|
|
skill=skill_lower,
|
|
success=outcome.check_result.success
|
|
)
|
|
|
|
return success_response(result=outcome.to_dict())
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
"check_error",
|
|
error=str(e),
|
|
check_type=check_type,
|
|
character_id=character_id
|
|
)
|
|
return error_response(
|
|
status_code=500,
|
|
message="Failed to perform check",
|
|
error_code="CHECK_ERROR"
|
|
)
|
|
|
|
|
|
@game_mechanics_bp.route('/skills', methods=['GET'])
|
|
def list_skills():
|
|
"""
|
|
List all available skill types.
|
|
|
|
Returns the skill types available for skill checks,
|
|
along with their associated base stats.
|
|
|
|
Returns:
|
|
{
|
|
"skills": [
|
|
{
|
|
"name": "perception",
|
|
"stat": "wisdom",
|
|
"description": "..."
|
|
},
|
|
...
|
|
]
|
|
}
|
|
"""
|
|
skills = []
|
|
for skill in SkillType:
|
|
skills.append({
|
|
"name": skill.name.lower(),
|
|
"stat": skill.value,
|
|
})
|
|
|
|
return success_response(result={"skills": skills})
|
|
|
|
|
|
@game_mechanics_bp.route('/difficulties', methods=['GET'])
|
|
def list_difficulties():
|
|
"""
|
|
List all difficulty levels and their DC values.
|
|
|
|
Returns:
|
|
{
|
|
"difficulties": [
|
|
{"name": "trivial", "dc": 5},
|
|
{"name": "easy", "dc": 10},
|
|
...
|
|
]
|
|
}
|
|
"""
|
|
difficulties = []
|
|
for diff in Difficulty:
|
|
difficulties.append({
|
|
"name": diff.name.lower(),
|
|
"dc": diff.value,
|
|
})
|
|
|
|
return success_response(result={"difficulties": difficulties})
|