first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
"""
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})