Phase 14: LLM-grounded encounter-balancing tip

- src/lib/assistant/prompts.ts: balanceSuggestionSchema + buildBalancePrompt
  (system message leads with the grounding constraint; restricts choices to the
  provided compendium candidates).
- useEncounterAdvisor hook: builds context → picks system-correct candidates →
  asks the LLM (when configured) for a structured pick, validates + filters names
  to the candidate set, else falls back to the deterministic buildSuggestedEncounter.
  Apply adds the creatures transactionally via encountersRepo.mutate.
- EncounterTipCard in the combat tracker: leads with the detected difficulty
  tendency (or the current difficulty), offers a one-click grounded suggestion
  (AI or deterministic) with propose→confirm Apply.

prompts unit tests + e2e for both the deterministic apply flow and the AI path
(stubbed provider); the AI test also confirms candidate-grounding (only
shortlisted creatures survive).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 09:17:23 +02:00
parent f84e429ca4
commit dbbf68752e
7 changed files with 407 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
import { z } from 'zod';
import type { CampaignContext, CreatureCandidate } from './context';
export const balanceSuggestionSchema = z.object({
reasoning: z.string(),
add: z.array(z.object({
name: z.string(),
count: z.number().int().min(1).max(8),
ref: z.string().optional(),
})),
targetDifficulty: z.string(),
});
export type BalanceSuggestion = z.infer<typeof balanceSuggestionSchema>;
function partyLine(ctx: CampaignContext): string {
return ctx.party.map((p) => `${p.name} (level ${p.level} ${p.className})`).join(', ') || 'no PCs recorded';
}
/**
* Prompt for rebalancing the current encounter. The system message leads with the
* grounding constraint and restricts choices to the provided candidate list.
*/
export function buildBalancePrompt(
ctx: CampaignContext,
current: { difficulty: string; targetDifficulty: string; candidates: CreatureCandidate[] },
): { system: string; user: string } {
const system =
`${ctx.systemConstraint}\n\n` +
`You are an encounter-balancing assistant for a ${ctx.systemLabel} game master. ` +
`Recommend which creatures to ADD to the current encounter to bring it to roughly "${current.targetDifficulty}" difficulty. ` +
`You MUST choose only from the provided candidate creatures (by exact name). Do not invent creatures or use any from another system. ` +
`Prefer 13 distinct creatures with sensible counts. Keep the reasoning to one or two sentences.`;
const candidateList = current.candidates
.map((c) => `- ${c.name} (rating ${c.rating}${c.hp ? `, hp ${c.hp}` : ''}${c.ac ? `, ac ${c.ac}` : ''})`)
.join('\n');
const user =
`Party: ${partyLine(ctx)}.\n` +
`Current encounter difficulty: ${current.difficulty}.\n` +
`Target difficulty: ${current.targetDifficulty}.\n\n` +
`Candidate creatures (choose only from these):\n${candidateList}\n\n` +
`Respond with the creatures to add (name + count), a short reasoning, and the targetDifficulty.`;
return { system, user };
}