stepstead-backend/routers/world.py
jts ea049b8ccd İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:24 +03:00

194 lines
7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
World: pedometer adım senkronizasyonu + günlük hedef/streak.
NOT (2026-07-04): Tile/chunk sistemi kaldırıldı — dünya artık lokasyon-graf
(bkz. routers/travel.py: /world/map, /world/travel, /world/gather, /world/event/resolve).
"""
from datetime import date, datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from deps import get_current_user
from models import Character, User, get_db
router = APIRouter(prefix="/world", tags=["world"])
# Pedometer adım senkronizasyonu (gerçek cihaz)
STEP_CAP_PER_MIN = 250 # dakika başı kabul edilen maks adım (koşu dahil)
STEP_SYNC_GRACE = 500 # her sync'te elapsed-tavanına eklenen sabit pay
STEP_FIRST_SYNC_MAX = 5000 # ilk sync / last_sync_at boşken kabul tavanı
STEP_SYNC_MIN_INTERVAL = 10 # iki sync arası min saniye (spam koruması)
# Günlük hedef + streak
DAILY_GOAL = 3000 # günlük gerçek adım hedefi
DAILY_BONUS_BASE = 200 # hedef ödülü (adım bakiyesi)
DAILY_BONUS_PER_STREAK = 50 # streak günü başına ek (7 günde tavan)
DAILY_STREAK_CAP = 7
DAILY_GOLD_BONUS = 5 # hedef ödülü (gold)
def _roll_daily(char: Character, today: date) -> None:
"""Gün değiştiyse günlük sayacı sıfırla; ödül dünden alınmadıysa streak kırılır."""
if char.daily_date != today:
char.daily_date = today
char.daily_steps = 0
if char.last_claim_date is not None and (today - char.last_claim_date).days > 1:
char.streak_days = 0
class SyncStepsIn(BaseModel):
delta: int = Field(ge=0, le=200000) # son sync'ten beri eklenen adım
source: str = Field("unknown", max_length=32)
class SyncStepsOut(BaseModel):
step_balance: int
step_lifetime: int
accepted: int
rejected: int
reason: Optional[str] = None
@router.post("/sync-steps", response_model=SyncStepsOut)
async def sync_steps(d: SyncStepsIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
if not char:
raise HTTPException(404, "Karakter yok")
now = datetime.now(timezone.utc)
last = char.last_sync_at
reason = None
# tavan hesabı
if last is None:
cap = STEP_FIRST_SYNC_MAX
else:
elapsed = (now - last).total_seconds()
if elapsed < STEP_SYNC_MIN_INTERVAL and d.delta > 0:
# çok sık sync — yine de kabul et ama küçük tavan uygula
elapsed = STEP_SYNC_MIN_INTERVAL
cap = int(elapsed / 60.0 * STEP_CAP_PER_MIN) + STEP_SYNC_GRACE
accepted = min(d.delta, cap)
rejected = d.delta - accepted
if rejected > 0:
reason = "cap_exceeded: delta=%d cap=%d" % (d.delta, cap)
_roll_daily(char, now.date())
if accepted > 0:
char.step_balance += accepted
char.step_lifetime += accepted
char.daily_steps += accepted
char.last_sync_at = now
src = (d.source or "unknown")[:32]
if d.delta > 0:
await db.execute(text(
"INSERT INTO step_log (character_id, steps_delta, source, accepted, reason) "
"VALUES (:cid, :sd, :src, :acc, :rsn)"
), {"cid": char.id, "sd": d.delta, "src": src, "acc": rejected == 0, "rsn": reason})
await db.commit()
return SyncStepsOut(
step_balance=char.step_balance,
step_lifetime=char.step_lifetime,
accepted=accepted,
rejected=rejected,
reason=reason,
)
# DEBUG: adım yükleme (pedometer gelene kadar test için)
class DebugAddStepsIn(BaseModel):
steps: int = Field(gt=0, le=10000)
@router.post("/debug/add-steps", response_model=dict)
async def debug_add_steps(d: DebugAddStepsIn, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
if not char:
raise HTTPException(404, "Karakter yok")
_roll_daily(char, datetime.now(timezone.utc).date())
char.step_balance += d.steps
char.step_lifetime += d.steps
char.daily_steps += d.steps
await db.commit()
return {"step_balance": char.step_balance, "step_lifetime": char.step_lifetime}
# ---------- Günlük hedef + streak ----------
class DailyOut(BaseModel):
goal: int
today_steps: int
streak_days: int
claimable: bool
claimed_today: bool
bonus_steps: int # şu anki streak'e göre bugünkü ödül
bonus_gold: int
def _bonus_for(streak_after_claim: int) -> int:
return DAILY_BONUS_BASE + DAILY_BONUS_PER_STREAK * min(streak_after_claim, DAILY_STREAK_CAP)
def _daily_out(char: Character, today: date) -> DailyOut:
claimed_today = char.last_claim_date == today
claimable = (not claimed_today) and char.daily_steps >= DAILY_GOAL
next_streak = char.streak_days if claimed_today else char.streak_days + 1
return DailyOut(
goal=DAILY_GOAL, today_steps=char.daily_steps, streak_days=char.streak_days,
claimable=claimable, claimed_today=claimed_today,
bonus_steps=_bonus_for(next_streak), bonus_gold=DAILY_GOLD_BONUS,
)
@router.get("/daily", response_model=DailyOut)
async def daily_status(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
if not char:
raise HTTPException(404, "Karakter yok")
today = datetime.now(timezone.utc).date()
_roll_daily(char, today)
await db.commit()
return _daily_out(char, today)
class DailyClaimOut(BaseModel):
streak_days: int
bonus_steps: int
bonus_gold: int
step_balance: int
gold: int
@router.post("/daily/claim", response_model=DailyClaimOut)
async def daily_claim(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
char = (await db.execute(select(Character).where(Character.user_id == user.id))).scalar_one_or_none()
if not char:
raise HTTPException(404, "Karakter yok")
today = datetime.now(timezone.utc).date()
_roll_daily(char, today)
if char.last_claim_date == today:
raise HTTPException(400, "Bugünkü ödülü zaten aldın")
if char.daily_steps < DAILY_GOAL:
raise HTTPException(400, f"Hedef dolmadı: {char.daily_steps}/{DAILY_GOAL}")
char.streak_days += 1
char.last_claim_date = today
bonus = _bonus_for(char.streak_days)
char.step_balance += bonus
char.gold += DAILY_GOLD_BONUS
await db.execute(text(
"INSERT INTO step_log (character_id, steps_delta, source, accepted, reason) "
"VALUES (:cid, :sd, 'daily_bonus', true, :rsn)"
), {"cid": char.id, "sd": bonus, "rsn": f"streak={char.streak_days}"})
await db.commit()
return DailyClaimOut(
streak_days=char.streak_days, bonus_steps=bonus, bonus_gold=DAILY_GOLD_BONUS,
step_balance=char.step_balance, gold=char.gold,
)