diff --git a/backend/app/api/projects.py b/backend/app/api/projects.py index e3b8ed1..2641a98 100644 --- a/backend/app/api/projects.py +++ b/backend/app/api/projects.py @@ -10,7 +10,7 @@ from app.realtime import publish from app.schemas import ProjectCreate, ProjectOut, ProjectUpdate from app.services.detailing import summarize_project -from app.services.xp import coins_for_create, grant_create_xp +from app.services.xp import XP_CREATE_PROJECT, coins_for_create, grant_create_xp router = APIRouter(prefix="/api/projects", tags=["projects"]) @@ -48,8 +48,8 @@ user_id=user_id, source="create_project", amount=coins_for_create("create_project") ) ) - response.headers["X-Created-XP"] = "5" - response.headers["X-Created-Coins"] = "2" + response.headers["X-Created-XP"] = str(XP_CREATE_PROJECT) + response.headers["X-Created-Coins"] = str(coins_for_create("create_project")) db.commit() # суммаризация заметки для контекста LLM (ТЗ 3.11) — фон, коммит уже сделан if (schema.note or "").strip(): diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 6af04f1..647b312 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -21,6 +21,7 @@ from app.services.detailing import apply_proposal, detail_task from app.services.options import pick_options from app.services.xp import ( + XP_CREATE_TASK, coins_for_create, grant_create_xp, ) @@ -110,8 +111,8 @@ # заголовки X-Created-* фронт буферизует отдельно от награды за закрытие grant_create_xp(db, "create_task", user_id) db.add(CoinEvent(user_id=user_id, source="create_task", amount=coins_for_create("create_task"))) - response.headers["X-Created-XP"] = "2" - response.headers["X-Created-Coins"] = "1" + response.headers["X-Created-XP"] = str(XP_CREATE_TASK) + response.headers["X-Created-Coins"] = str(coins_for_create("create_task")) # Коммитим до планирования фоновой работы: BackgroundTasks выполняются ДО # teardown-коммита зависимости get_db, а detail_task открывает свою сессию. db.commit() diff --git a/backend/app/services/garden.py b/backend/app/services/garden.py index 4bc5858..776fa46 100644 --- a/backend/app/services/garden.py +++ b/backend/app/services/garden.py @@ -17,11 +17,13 @@ # --- Экономика --- -# Монеты за задачу: floor(xp / 2) -COINS_PER_XP = 2 +# Монеты за задачу: xp целиком (ребаланс 0.48 — было floor(xp / 2): сад +# «немного» подешевел, экономика ускорилась примерно вдвое) +COINS_PER_XP = 1 -# Бонус за достижение уровня L (одним событием при пересечении границы) -LEVEL_BONUS = 25 +# Бонус за достижение уровня L (одним событием при пересечении границы); +# ребаланс 0.48 — было 25 +LEVEL_BONUS = 50 def coins_for_xp(xp: int) -> int: diff --git a/backend/app/services/xp.py b/backend/app/services/xp.py index 4448657..6dfd4ef 100644 --- a/backend/app/services/xp.py +++ b/backend/app/services/xp.py @@ -24,10 +24,11 @@ WILLPOWER_BONUS = 10 # Микронаграды за создание (ТЗ 3.13): капля XP/монет, чтобы захват был заметен +# (ребаланс 0.48: монеты ×2 — было 1 и 2) XP_CREATE_TASK = 2 -COINS_CREATE_TASK = 1 +COINS_CREATE_TASK = 2 XP_CREATE_PROJECT = 5 -COINS_CREATE_PROJECT = 2 +COINS_CREATE_PROJECT = 4 # Ежедневная награда за визит (раз в сутки, только монеты) DAILY_COINS = 50 diff --git a/backend/tests/test_garden.py b/backend/tests/test_garden.py index d12a66b..0823aec 100644 --- a/backend/tests/test_garden.py +++ b/backend/tests/test_garden.py @@ -47,8 +47,8 @@ assert earned2["xp"] == "" and earned2["coins"] == "" state = client.get("/api/garden").json() - # баланс = монеты за закрытие + микронаграда за создание задачи (+1) - assert state["balance"] == coins_for_xp(int(earned["xp"])) + 1 + # баланс = монеты за закрытие + микронаграда за создание задачи (+2) + assert state["balance"] == coins_for_xp(int(earned["xp"])) + 2 def test_level_bonus_coins(client: TestClient) -> None: @@ -56,8 +56,8 @@ for _ in range(3): task_id = _create_task(client, priority=9, estimated_minutes=480) _done(client, task_id) - # Монеты: 3 × 20 за задачи + 3 × 1 за создание + бонус 25×2 = 113 - assert client.get("/api/garden").json()["balance"] == 113 + # Монеты: 3 × 40 за задачи + 3 × 2 за создание + бонус 50×2 = 226 + assert client.get("/api/garden").json()["balance"] == 226 # --- Перемещение --- @@ -87,23 +87,23 @@ # Редкость случайна — фиксируем roll: всегда common monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99) task_id = _create_task(client, priority=9, estimated_minutes=480) - _done(client, task_id) # 40 XP → 20 монет + _done(client, task_id) # 40 XP → 40 монет plant = next(i for i in client.get("/api/garden").json()["items"] if i["kind"] == "plant") assert plant["rarity"] == "common" - # Первая стадия ровно по карману (20 = 20, +1 за создание задачи) + # Первая стадия по карману (40 монет за закрытие + 2 за создание − 20) resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade") assert resp.status_code == 200 assert resp.json()["stage"] == 1 - assert resp.json()["balance"] == 1 + assert resp.json()["balance"] == 22 - # Вторая стадия — не хватает (50 > 1) + # Вторая стадия — не хватает (50 > 22) resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade") assert resp.status_code == 402 assert "50" in resp.json()["detail"] for _ in range(10): - _done(client, _create_task(client)) # 10×5 монет + создания + бонус L2 — с запасом + _done(client, _create_task(client)) # 10×10 монет + создания + бонус L2 — с запасом resp = client.post(f"/api/garden/plants/{plant['id']}/upgrade") assert resp.status_code == 200 assert resp.json()["stage"] == 2 @@ -119,12 +119,12 @@ def test_shop_buy_decoration_and_repeatable(client: TestClient, monkeypatch: Any) -> None: monkeypatch.setattr("app.services.xp.random.random", lambda: 0.99) _done(client, _create_task(client, priority=9, estimated_minutes=480)) - _done(client, _create_task(client, priority=9, estimated_minutes=480)) # 40 монет + _done(client, _create_task(client, priority=9, estimated_minutes=480)) # 80 монет resp = client.post("/api/garden/shop/buy", json={"item_key": "fence"}) assert resp.status_code == 200 balance_after = resp.json()["balance"] - # 2 закрытия по 20 + 2 создания по 1 − изгородь - assert balance_after == 40 + 2 - DECORATIONS["fence"]["cost"] + # 2 закрытия по 40 + 2 создания по 2 − изгородь + assert balance_after == 80 + 4 - DECORATIONS["fence"]["cost"] # Покупка кладёт декорацию в инвентарь: элемент есть, координат нет item = next( i @@ -249,7 +249,7 @@ def test_level_bonus_helper() -> None: - assert level_bonus_coins(2) == 50 - assert coins_for_xp(40) == 20 + assert level_bonus_coins(2) == 100 + assert coins_for_xp(40) == 40 # ТЗ 0.37: лестница расширений сокращена до двух шагов assert set(EXPANSIONS) == {"expansion_1", "expansion_2"} diff --git a/backend/tests/test_xp.py b/backend/tests/test_xp.py index ed7de84..bb3be54 100644 --- a/backend/tests/test_xp.py +++ b/backend/tests/test_xp.py @@ -20,7 +20,7 @@ # Создание задачи — микронаграда (заголовки X-Created-*) resp_create = client.post("/api/tasks", json={"title": "Задача для XP"}) assert resp_create.headers.get("X-Created-XP") == "2" - assert resp_create.headers.get("X-Created-Coins") == "1" + assert resp_create.headers.get("X-Created-Coins") == "2" task_id = resp_create.json()["id"] resp = client.patch(f"/api/tasks/{task_id}", json={"status": "done"}) assert resp.status_code == 200 @@ -157,7 +157,7 @@ resp = client.post("/api/projects", json={"name": "Проект награды"}) assert resp.status_code == 200 assert resp.headers.get("X-Created-XP") == "5" - assert resp.headers.get("X-Created-Coins") == "2" + assert resp.headers.get("X-Created-Coins") == "4" # XP проекта в суммарный опыт входит, счётчик закрытий — нет summary = client.get("/api/xp").json() @@ -168,7 +168,7 @@ # Сад: событие создания не рождает растение state = client.get("/api/garden").json() - assert state["balance"] == 2 + assert state["balance"] == 4 assert not [i for i in state["items"] if i["kind"] == "plant"] @@ -179,7 +179,7 @@ assert summary["completed_count"] == 0 state = client.get("/api/garden").json() - assert state["balance"] == 1 + assert state["balance"] == 2 assert not [i for i in state["items"] if i["kind"] == "plant"] diff --git a/docs/TZ.md b/docs/TZ.md index 9865042..f77ced9 100644 --- a/docs/TZ.md +++ b/docs/TZ.md @@ -4,7 +4,7 @@ | | | |---|---| -| Версия ТЗ | 0.47 | +| Версия ТЗ | 0.48 | | Дата | 2026-09-23 | | Статус | На обсуждении | @@ -284,7 +284,8 @@ (поле `kind` разделяет события: `task` — закрытие, `create_task` / `create_project` — создание). - **Микронаграды за создание** (без конфетти — скромный тост «+N XP • +M монет»): - создание задачи — +2 XP и +1 монета, создание проекта — +5 XP и +2 монеты. + создание задачи — +2 XP и +2 монеты, создание проекта — +5 XP и +4 монеты + (0.48: монеты ×2). В суммарный опыт (уровень) входят, растений в саду не рождают, счётчик закрытых задач не увеличивают; хранятся с `task_id = NULL` — удаление задачи или проекта награду не отнимает. @@ -330,9 +331,11 @@ принцип: **контент открывается за опыт, покупается за монеты** — опыт (уровень) открывает виды растений и землю, монеты покупают рост растений, декорации и расширения карты. -- **Монеты**: начисляются при закрытии задачи — `floor(xp / 2)` (заголовок - `X-Earned-Coins` рядом с XP в тосте), и бонусом **25 × L** при достижении - уровня L. Хранение — таблица `coin_events` (журнал заработка и трат; баланс = +- **Монеты**: начисляются при закрытии задачи — xp целиком (0.48: было + `floor(xp / 2)` — экономика сада ускорена примерно вдвое; заголовок + `X-Earned-Coins` рядом с XP в тосте), и бонусом **50 × L** при достижении + уровня L (0.48: было 25 × L). Ежедневная награда не менялась (50 монет). + Хранение — таблица `coin_events` (журнал заработка и трат; баланс = сумма amount). Траты — отрицательные события (`source = shop`); продажи нет («только позитив»). - **Редкие растения**: редкость решается в момент закрытия по весу задачи —