AgentSkillsCN

game-mechanics

叙事卡游戏规则的实现与调试参考:事实(公开/秘密/已解决的生命周期)、计分规则(事实解决得分 + 主题加成)、场景推进(9个场景 × 3幕,聚光灯轮换,场景结束条件),以及卡牌规则的派发机制。在添加新卡牌效果、实现事实操作、调试计分逻辑,或配置场景转换时使用。触发词:事实、解决、复杂化、揭示、回调、计分、场景、幕、紧张感、连贯性、聚光灯、阶段转换。

SKILL.md
--- frontmatter
name: game-mechanics
description: "Reference for implementing and debugging Narrative Cards game rules: Facts (open/secret/resolved lifecycle), scoring (Fact resolution points + theme bonus), scene progression (9 scenes × 3 acts, spotlight rotation, scene-end conditions), and card rule dispatch. Use when adding new card effects, implementing Fact operations, debugging scoring, or wiring scene transitions. Trigger words: fact, resolve, complicate, reveal, callback, score, scene, act, tension, coherence, spotlight, phase transition."
argument-hint: "Mechanic to look up (e.g., 'resolve fact', 'scoring', 'scene 9')"
user-invocable: true

Game Mechanics Reference

When to Use

  • Implementing a new card type or move card effect
  • Adding or debugging Fact operations (create, complicate, reveal, resolve)
  • Wiring scene progression or phase transitions
  • Debugging meter behaviour or scoring calculations
  • Understanding what happens at scene 9 / game end

Fact Lifecycle

See fact-lifecycle.md for the full specification.

TL;DR:

  • element cards → create a Fact (isSecret if subtype === 'secret')
  • Introduce move → create an open Fact
  • Complicate move → append to fact.complications[], Tension +1
  • Callback move → increment fact.callbacks
  • Reveal move → set fact.revealed = true (secret facts only)
  • Resolve move → costs 2 Tension; resolver +2 pts, creator +1 pt; fact.status = 'resolved'

Scoring

See scoring.md for the full specification.

TL;DR:

  • In-play points: Resolve = +2 to resolver, +1 to creator
  • Theme bonus: 0–3 pts per player, manually awarded in scoring phase
  • player.totalScore = player.points + player.themeBonus
  • Never auto-award theme bonuses

Scene Progression

Structure

  • 9 scenes total: scenes 1–3 (Act 1), 4–6 (Act 2), 7–9 (Act 3)
  • Act is derived: Math.ceil(sceneCount / 3)

Per-scene flow

  1. Spotlight player is set at scene start (spotlightIndex % players.length)
  2. Each player plays exactly one card (tracked via currentScene.cardsPlayed)
  3. Scene completes when cardsPlayed.length === players.length
  4. Phase transitions to 'scene-end' — players review the scene
  5. nextScene() advances to the next scene or to 'scoring'

Scene completion condition

typescript
const allPlayed = currentScene.cardsPlayed.length >= players.length;

Only allow nextScene() when allPlayed === true.

Scene 9 special rule

Before completing scene 9, a Resolve move must have been played:

typescript
const hasResolve = currentScene.cardsPlayed.some(
  (cp) =>
    players.flatMap((p) => p.hand).find((c) => c.id === cp.cardId)?.name ===
    "Resolve",
);
// Or check via the log for the current scene

Store this as gameState.needsResolution: boolean — never recompute in screen components.

Phase transition map

code
setup  →  draft  →  play  ↔  scene-end  →  play     (scenes 1–8)
                                         →  scoring  (after scene 9)
scoring  →  end

No backward transitions except play ↔ scene-end.

Meters

MeterMinMaxEffect tokens
Tension010+N Tension, -N Tension
Coherence010+N Coherence, -N Coherence

Always cap with Math.min(10, Math.max(0, value)).

Card Rule Dispatch

Dispatched primarily by card.type, then by card.name for moves:

code
card.type === 'element'  →  create Fact (secret if subtype === 'secret')
card.type === 'event'    →  parse effect string for meter tokens
card.type === 'move'     →  dispatch by card.name:
  'Introduce'  → create open Fact
  'Complicate' → add complication to target Fact, +1 Tension
  'Reveal'     → reveal a secret Fact
  'Callback'   → increment callbacks on target Fact
  'Resolve'    → resolve target Fact (-2 Tension, points)
  'Confront'   → scene climax mechanic (trigger ContradictionModal or NPC)

Effect String Parsing

Event effect strings use substring checks. Canonical tokens:

TokenEffect
+N Tensiontension += N
-N Tensiontension -= N
+N Coherencecoherence += N
-N Coherencecoherence -= N
Choose: A OR BPlayer selects one option; only selected option applied

Parse example:

typescript
function parseMeterDeltas(effect: string) {
  const tensionMatch = effect.match(/([+-]\d+)\s*Tension/);
  const coherenceMatch = effect.match(/([+-]\d+)\s*Coherence/);
  return {
    tension: tensionMatch ? parseInt(tensionMatch[1]) : 0,
    coherence: coherenceMatch ? parseInt(coherenceMatch[1]) : 0,
  };
}

For Choose: effects, parse only the selected option string, not the full effect.

Spotlight Rotation

typescript
const nextSpotlight = (prev.spotlightIndex + 1) % prev.players.length;

The spotlight player index is stored in gameState.spotlightIndex. Screens derive the current player name from players[spotlightIndex].name.