jev-chess 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/ARCHITECTURE.md +148 -0
  2. package/README.md +109 -0
  3. package/dist/chessEngine.d.ts +36 -0
  4. package/dist/chessEngine.d.ts.map +1 -0
  5. package/dist/chessEngine.js +173 -0
  6. package/dist/chessEngine.js.map +1 -0
  7. package/dist/classicMatches.d.ts +50 -0
  8. package/dist/classicMatches.d.ts.map +1 -0
  9. package/dist/classicMatches.js +223 -0
  10. package/dist/classicMatches.js.map +1 -0
  11. package/dist/demo.d.ts +2 -0
  12. package/dist/demo.d.ts.map +1 -0
  13. package/dist/demo.js +142 -0
  14. package/dist/demo.js.map +1 -0
  15. package/dist/gameReviewer.d.ts +11 -0
  16. package/dist/gameReviewer.d.ts.map +1 -0
  17. package/dist/gameReviewer.js +89 -0
  18. package/dist/gameReviewer.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/moveEvaluator.d.ts +11 -0
  24. package/dist/moveEvaluator.d.ts.map +1 -0
  25. package/dist/moveEvaluator.js +103 -0
  26. package/dist/moveEvaluator.js.map +1 -0
  27. package/dist/moveResolver.d.ts +15 -0
  28. package/dist/moveResolver.d.ts.map +1 -0
  29. package/dist/moveResolver.js +97 -0
  30. package/dist/moveResolver.js.map +1 -0
  31. package/dist/personaEngine.d.ts +15 -0
  32. package/dist/personaEngine.d.ts.map +1 -0
  33. package/dist/personaEngine.js +151 -0
  34. package/dist/personaEngine.js.map +1 -0
  35. package/dist/server.d.ts +2 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +1490 -0
  38. package/dist/server.js.map +1 -0
  39. package/dist/typeSafeClient.d.ts +13 -0
  40. package/dist/typeSafeClient.d.ts.map +1 -0
  41. package/dist/typeSafeClient.js +336 -0
  42. package/dist/typeSafeClient.js.map +1 -0
  43. package/dist/types.d.ts +100 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +5 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/web/main.d.ts +2 -0
  48. package/dist/web/main.d.ts.map +1 -0
  49. package/dist/web/main.js +620 -0
  50. package/dist/web/main.js.map +1 -0
  51. package/package.json +35 -0
  52. package/public/app.js +45 -0
  53. package/public/index.html +790 -0
  54. package/src/chessEngine.ts +191 -0
  55. package/src/classicMatches.ts +291 -0
  56. package/src/demo.ts +160 -0
  57. package/src/gameReviewer.ts +113 -0
  58. package/src/index.ts +8 -0
  59. package/src/moveEvaluator.ts +122 -0
  60. package/src/moveResolver.ts +120 -0
  61. package/src/personaEngine.ts +193 -0
  62. package/src/server.ts +1509 -0
  63. package/src/typeSafeClient.ts +321 -0
  64. package/src/types.ts +118 -0
  65. package/src/web/main.ts +615 -0
  66. package/tests/chess.test.ts +206 -0
  67. package/tsconfig.json +21 -0
@@ -0,0 +1,113 @@
1
+ import type { ChessEngine } from "./chessEngine.js";
2
+ import { choice, getTypeSafeClient, score } from "./typeSafeClient.js";
3
+ import type { AnnotatedMove, BlunderDiagnosis, MistakeArchetype } from "./types.js";
4
+
5
+ const MISTAKE_ARCHETYPES: Record<MistakeArchetype, string> = {
6
+ sound_move: "A solid, principled move with no critical defect",
7
+ tactical_blindness: "Overlooked an immediate tactical blow, fork, pin, skewer, or check",
8
+ poisoned_pawn_greed: "Grabbed material at the fatal expense of development or king safety",
9
+ over_extension: "Pushed pawns or pieces too far forward, creating irreparable structural holes",
10
+ passive_concession: "Retreated timidly, yielding tempo and spatial dominance to the opponent",
11
+ king_safety_negligence: "Compromised the pawn shield or exposed the monarch to a deadly storm",
12
+ };
13
+
14
+ const DEFENSIVE_DIFFICULTY_RUBRIC = [
15
+ "Comfortable defense; natural and straightforward moves maintain balance",
16
+ "Tense defense; requires disciplined calculation and piece coordination",
17
+ "Precise tightrope; requires narrow, counter-intuitive only-moves to survive",
18
+ "Hopeless or practically impossible for a human player to defend under pressure",
19
+ ] as const;
20
+
21
+ const TENSION_RUBRIC = [
22
+ "Peaceful and quiet; strategic maneuvering behind closed lines",
23
+ "Building central friction and contested outposts",
24
+ "Sharp tactical skirmish; pieces in contact and multiple captures pending",
25
+ "Decisive boiling point; mutual king attacks and explosive complications",
26
+ ] as const;
27
+
28
+ /**
29
+ * Diagnostic Game Reviewer that classifies chess moves and blunders into actionable semantic concepts.
30
+ * Pattern: Verification & Escalation / Structured Diagnostics.
31
+ */
32
+ export class GameReviewer {
33
+ private get client() {
34
+ return getTypeSafeClient();
35
+ }
36
+
37
+ public async diagnoseMove(
38
+ engine: ChessEngine,
39
+ move: AnnotatedMove,
40
+ moveNumber: number
41
+ ): Promise<BlunderDiagnosis> {
42
+ const boardContext = engine.getBoardContext();
43
+
44
+ const state = {
45
+ game_state: {
46
+ fen: boardContext.fen,
47
+ turn: boardContext.turn,
48
+ phase: boardContext.gamePhase,
49
+ material: boardContext.materialBalance.description,
50
+ recent_moves: boardContext.recentHistory,
51
+ },
52
+ played_move: {
53
+ san: move.san,
54
+ description: move.description,
55
+ is_capture: move.isCapture,
56
+ is_check: move.isCheck,
57
+ },
58
+ };
59
+
60
+ const response = await this.client.systemOne({
61
+ state,
62
+ questions: {
63
+ archetype: choice(
64
+ "Which mistake or positional category best describes `played_move.san`?",
65
+ MISTAKE_ARCHETYPES
66
+ ),
67
+ defensive_difficulty: score(
68
+ "How difficult is it to defend or recover from this position after `played_move.san`?",
69
+ DEFENSIVE_DIFFICULTY_RUBRIC
70
+ ),
71
+ tension: score(
72
+ "What is the overall tactical and psychological tension level on the board?",
73
+ TENSION_RUBRIC
74
+ ),
75
+ },
76
+ });
77
+
78
+ const archetypeChoice = response.answers.archetype.choice as MistakeArchetype;
79
+ const diffScore = response.answers.defensive_difficulty.score;
80
+ const tensionScore = response.answers.tension.score;
81
+
82
+ let coachAdvice = "Keep developing actively and control the center.";
83
+ switch (archetypeChoice) {
84
+ case "tactical_blindness":
85
+ coachAdvice = "Always scan for opponent checks, captures, and threats before finalizing your move.";
86
+ break;
87
+ case "poisoned_pawn_greed":
88
+ coachAdvice = "Do not go pawn hunting with an uncastled king or uncoordinated pieces!";
89
+ break;
90
+ case "over_extension":
91
+ coachAdvice = "Pawns cannot move backwards; every advance creates irreversible weaknesses.";
92
+ break;
93
+ case "passive_concession":
94
+ coachAdvice = "When pushed, look for counter-attacking resources rather than passive retreats.";
95
+ break;
96
+ case "king_safety_negligence":
97
+ coachAdvice = "Castling and securing your king should take priority over peripheral adventures.";
98
+ break;
99
+ case "sound_move":
100
+ coachAdvice = "Excellent, harmonious play. Continue executing your strategic plan.";
101
+ break;
102
+ }
103
+
104
+ return {
105
+ moveNumber,
106
+ playedMove: move.san,
107
+ mistakeArchetype: archetypeChoice,
108
+ defensiveDifficulty: diffScore,
109
+ tacticalTension: tensionScore,
110
+ coachAdvice,
111
+ };
112
+ }
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./chessEngine.js";
3
+ export * from "./typeSafeClient.js";
4
+ export * from "./moveResolver.js";
5
+ export * from "./moveEvaluator.js";
6
+ export * from "./personaEngine.js";
7
+ export * from "./gameReviewer.js";
8
+ export * from "./classicMatches.js";
@@ -0,0 +1,122 @@
1
+ import type { ChessEngine } from "./chessEngine.js";
2
+ import { choice, getTypeSafeClient, noul, score } from "./typeSafeClient.js";
3
+ import type { AnnotatedMove, MoveEvaluation } from "./types.js";
4
+
5
+ const SHARPNESS_RUBRIC = [
6
+ "Quiet, solid, or prophylactic maneuver with minimal immediate confrontation",
7
+ "Normal central development or positional consolidation",
8
+ "Sharp or provocative move with direct tactical tension",
9
+ "High-risk sacrifice or explosive tactical clash",
10
+ ] as const;
11
+
12
+ const STRATEGIC_THEMES = {
13
+ pawn_break: "Advances a pawn to challenge or dissolve opponent pawn structure",
14
+ piece_activation: "Develops or repositions a piece to an active central or attacking square",
15
+ prophylaxis: "Prevents opponent's plan, stabilizes position, or secures king safety",
16
+ tactical_strike: "Direct capture, check, fork, pin, or tactic to win material or mate",
17
+ king_hunt: "Advances pieces or pawns directly against the enemy king",
18
+ simplification: "Trades pieces to consolidate advantage or clarify position",
19
+ };
20
+
21
+ /**
22
+ * Multi-dimensional qualitative evaluator for chess moves using TypeSafe AI.
23
+ * Pattern: Parallel Atomic Judgments + Code-Controlled Badge Synthesis.
24
+ */
25
+ export class MoveEvaluator {
26
+ private get client() {
27
+ return getTypeSafeClient();
28
+ }
29
+
30
+ public async evaluateMove(
31
+ engine: ChessEngine,
32
+ move: AnnotatedMove
33
+ ): Promise<MoveEvaluation> {
34
+ const boardContext = engine.getBoardContext();
35
+
36
+ const state = {
37
+ evaluated_move: {
38
+ san: move.san,
39
+ description: move.description,
40
+ is_capture: move.isCapture,
41
+ is_check: move.isCheck,
42
+ piece: move.piece,
43
+ },
44
+ board_context: {
45
+ fen: boardContext.fen,
46
+ turn: boardContext.turn,
47
+ game_phase: boardContext.gamePhase,
48
+ material: boardContext.materialBalance.description,
49
+ recent_history: boardContext.recentHistory,
50
+ },
51
+ };
52
+
53
+ // Parallel atomic evaluation in a single round-trip
54
+ const response = await this.client.systemOne({
55
+ state,
56
+ questions: {
57
+ tactical_sharpness: score(
58
+ "How sharp, tactical, or double-edged is the move `evaluated_move.san` in this position?",
59
+ SHARPNESS_RUBRIC
60
+ ),
61
+ strategic_theme: choice(
62
+ "Which primary strategic or tactical theme best characterizes `evaluated_move.san`?",
63
+ STRATEGIC_THEMES
64
+ ),
65
+ king_attack: noul(
66
+ "Does `evaluated_move.san` initiate or advance a direct attack against the enemy king?"
67
+ ),
68
+ psychological_pressure: noul(
69
+ "Is `evaluated_move.san` a provocative or high-pressure move that challenges the opponent mentally?"
70
+ ),
71
+ },
72
+ });
73
+
74
+ const sharpnessAnswer = response.answers.tactical_sharpness;
75
+ const themeAnswer = response.answers.strategic_theme;
76
+ const kingAnswer = response.answers.king_attack;
77
+ const pressureAnswer = response.answers.psychological_pressure;
78
+
79
+ const sharpnessScore = sharpnessAnswer.score;
80
+ const sharpnessLevel =
81
+ SHARPNESS_RUBRIC[Math.min(SHARPNESS_RUBRIC.length - 1, Math.round(sharpnessScore))] ??
82
+ "Standard move";
83
+
84
+ // Determine commentary badge in deterministic code (clean, professional text)
85
+ let commentaryBadge = "Standard Move";
86
+ if (sharpnessScore >= 2.2) {
87
+ commentaryBadge = "Sharp Tactical Clash";
88
+ } else if (kingAnswer.noul >= 0.7) {
89
+ commentaryBadge = "King Assault Initiated";
90
+ } else if (themeAnswer.choice === "prophylaxis") {
91
+ commentaryBadge = "Prophylactic Squeeze";
92
+ } else if (themeAnswer.choice === "pawn_break") {
93
+ commentaryBadge = "Central Pawn Break";
94
+ } else if (themeAnswer.choice === "simplification") {
95
+ commentaryBadge = "Positional Simplification";
96
+ } else if (sharpnessScore <= 0.6) {
97
+ commentaryBadge = "Calm Positional Maneuver";
98
+ } else {
99
+ commentaryBadge = "Piece Activation";
100
+ }
101
+
102
+ return {
103
+ san: move.san,
104
+ tacticalSharpness: {
105
+ score: sharpnessScore,
106
+ level: sharpnessLevel,
107
+ confidence: sharpnessAnswer.confidence,
108
+ },
109
+ strategicTheme: {
110
+ theme: themeAnswer.choice,
111
+ confidence: themeAnswer.confidence,
112
+ },
113
+ kingAttackRisk: {
114
+ probability: kingAnswer.noul,
115
+ },
116
+ psychologicalPressure: {
117
+ probability: pressureAnswer.noul,
118
+ },
119
+ commentaryBadge,
120
+ };
121
+ }
122
+ }
@@ -0,0 +1,120 @@
1
+ import type { ChessEngine } from "./chessEngine.js";
2
+ import { choice, getTypeSafeClient } from "./typeSafeClient.js";
3
+ import type { AnnotatedMove, MoveIntentResult } from "./types.js";
4
+
5
+ /**
6
+ * Resolves natural language user commands into exact legal chess moves using TypeSafe Choice.
7
+ * Pattern: Select Instead of Generate + Confidence-Gated Ambiguity Resolution.
8
+ */
9
+ export class MoveResolver {
10
+ private get client() {
11
+ return getTypeSafeClient();
12
+ }
13
+
14
+ /**
15
+ * Resolves natural language intent (e.g. "push my e-pawn two squares", "castle kingside")
16
+ * into a verified legal move.
17
+ */
18
+ public async resolveIntent(
19
+ engine: ChessEngine,
20
+ userQuery: string,
21
+ confidenceThreshold = 0.55
22
+ ): Promise<MoveIntentResult> {
23
+ const candidates = engine.getAnnotatedMoves();
24
+
25
+ if (candidates.length === 0) {
26
+ return {
27
+ query: userQuery,
28
+ matchedMove: null,
29
+ san: "none",
30
+ confidence: 1.0,
31
+ probabilities: {},
32
+ clarificationNeeded: false,
33
+ alternativeCandidates: [],
34
+ };
35
+ }
36
+
37
+ // Direct exact match fast-path (e.g. user literally typed "e4" or "Nf3")
38
+ const exactMatch = candidates.find(
39
+ (m) =>
40
+ m.san.toLowerCase() === userQuery.trim().toLowerCase() ||
41
+ `${m.from}${m.to}`.toLowerCase() === userQuery.trim().toLowerCase()
42
+ );
43
+ if (exactMatch) {
44
+ return {
45
+ query: userQuery,
46
+ matchedMove: exactMatch,
47
+ san: exactMatch.san,
48
+ confidence: 1.0,
49
+ probabilities: { [exactMatch.san]: 1.0 },
50
+ clarificationNeeded: false,
51
+ alternativeCandidates: [],
52
+ };
53
+ }
54
+
55
+ // Build Choice criteria from legal moves
56
+ const criteria: Record<string, string> = {};
57
+ for (const c of candidates) {
58
+ criteria[c.san] = c.description;
59
+ }
60
+ criteria["none"] = "No legal candidate move matches what the player requested";
61
+
62
+ const state = {
63
+ user_intent: userQuery,
64
+ board: {
65
+ turn: engine.turn(),
66
+ in_check: engine.isCheck(),
67
+ fen: engine.fen(),
68
+ },
69
+ legal_moves: candidates.map((c) => ({
70
+ san: c.san,
71
+ description: c.description,
72
+ })),
73
+ };
74
+
75
+ const response = await this.client.systemOne({
76
+ state,
77
+ questions: {
78
+ matched_move: choice(
79
+ "Which legal chess move in `legal_moves` best fulfills the player's command in `user_intent`?",
80
+ criteria
81
+ ),
82
+ },
83
+ });
84
+
85
+ const answer = response.answers.matched_move;
86
+ const selectedSan = answer.choice;
87
+ const confidence = answer.confidence;
88
+ const probabilities = answer.probabilities as Record<string, number>;
89
+
90
+ // Sort alternatives by probability
91
+ const sortedOptions = Object.entries(probabilities)
92
+ .filter(([k]) => k !== "none" && k !== selectedSan)
93
+ .sort((a, b) => b[1] - a[1])
94
+ .map(([k]) => k);
95
+
96
+ if (selectedSan === "none" || confidence < confidenceThreshold) {
97
+ return {
98
+ query: userQuery,
99
+ matchedMove: null,
100
+ san: selectedSan,
101
+ confidence,
102
+ probabilities,
103
+ clarificationNeeded: true,
104
+ alternativeCandidates: sortedOptions.slice(0, 3),
105
+ };
106
+ }
107
+
108
+ const matched = candidates.find((c) => c.san === selectedSan) ?? null;
109
+
110
+ return {
111
+ query: userQuery,
112
+ matchedMove: matched,
113
+ san: selectedSan,
114
+ confidence,
115
+ probabilities,
116
+ clarificationNeeded: false,
117
+ alternativeCandidates: sortedOptions.slice(0, 2),
118
+ };
119
+ }
120
+ }
@@ -0,0 +1,193 @@
1
+ import type { ChessEngine } from "./chessEngine.js";
2
+ import { getTypeSafeClient, noul, score } from "./typeSafeClient.js";
3
+ import type {
4
+ AnnotatedMove,
5
+ DimensionScores,
6
+ PersonaDecision,
7
+ PersonaId,
8
+ PersonaProfile,
9
+ PersonaScoredMove,
10
+ } from "./types.js";
11
+
12
+ export const CHESS_PERSONAS: Record<PersonaId, PersonaProfile> = {
13
+ tal: {
14
+ id: "tal",
15
+ name: "Mikhail Tal",
16
+ title: "The Magician from Riga",
17
+ quote:
18
+ "You must take your opponent into a deep dark forest where 2+2=5, and the path leading out is only wide enough for one.",
19
+ weights: {
20
+ aggression: 0.45,
21
+ prophylaxis: -0.15,
22
+ complexity: 0.5,
23
+ materialGreed: -0.2,
24
+ },
25
+ },
26
+ petrosian: {
27
+ id: "petrosian",
28
+ name: "Tigran Petrosian",
29
+ title: "Iron Tigran",
30
+ quote: "Nothing can be done against me; my position is so solid.",
31
+ weights: {
32
+ aggression: 0.05,
33
+ prophylaxis: 0.65,
34
+ complexity: -0.3,
35
+ materialGreed: 0.2,
36
+ },
37
+ },
38
+ capablanca: {
39
+ id: "capablanca",
40
+ name: "José Raúl Capablanca",
41
+ title: "The Chess Machine",
42
+ quote:
43
+ "A master plays simple chess, puts pieces where they belong, and simplifies into a clean endgame.",
44
+ weights: {
45
+ aggression: 0.2,
46
+ prophylaxis: 0.35,
47
+ complexity: -0.4,
48
+ materialGreed: 0.45,
49
+ },
50
+ },
51
+ coffeehouse: {
52
+ id: "coffeehouse",
53
+ name: "Coffeehouse Gambiteer",
54
+ title: "Romantic Hustler",
55
+ quote: "Checks are free, pawn storms are mandatory, and danger lurks on every rank.",
56
+ weights: {
57
+ aggression: 0.55,
58
+ prophylaxis: -0.35,
59
+ complexity: 0.4,
60
+ materialGreed: -0.1,
61
+ },
62
+ },
63
+ };
64
+
65
+ const AGGRESSION_RUBRIC = [
66
+ "Passive retreat, backward consolidation, or defensive block",
67
+ "Neutral developing maneuver or quiet regrouping",
68
+ "Forward thrust, central pressure, or active threat creation",
69
+ "Direct sacrifice, attack on the king, or fierce tactical challenge",
70
+ ] as const;
71
+
72
+ const PROPHYLAXIS_RUBRIC = [
73
+ "Completely ignores king safety and opponent counterplay",
74
+ "Standard move with ordinary tactical exposure",
75
+ "Solid, patient reinforcement of weaknesses or squares",
76
+ "Deep prophylactic suppression of opponent counter-ideas",
77
+ ] as const;
78
+
79
+ const COMPLEXITY_RUBRIC = [
80
+ "Clarifies position, forces trades, or liquidates tension",
81
+ "Steady, orderly position with clear structural goals",
82
+ "Introduces sharp asymmetry, unbalances, and multiple branches",
83
+ "Maximum chaos: highly volatile tactical minefield",
84
+ ] as const;
85
+
86
+ /**
87
+ * AI Opponent decision engine powered by TypeSafe Composite Scoring.
88
+ * Pattern: Atomic Multi-Dimensional Scoring + Client-Side Persona Weights.
89
+ */
90
+ export class PersonaEngine {
91
+ private get client() {
92
+ return getTypeSafeClient();
93
+ }
94
+
95
+ /**
96
+ * Scores candidate moves and selects the top move according to the persona's style.
97
+ */
98
+ public async selectMove(
99
+ engine: ChessEngine,
100
+ personaId: PersonaId,
101
+ candidateLimit = 5
102
+ ): Promise<PersonaDecision> {
103
+ const persona = CHESS_PERSONAS[personaId];
104
+ const allMoves = engine.getAnnotatedMoves();
105
+
106
+ if (allMoves.length === 0) {
107
+ throw new Error("No legal moves available in current position.");
108
+ }
109
+
110
+ // Candidate selection: curate a diverse pool representing varied styles
111
+ // (tactical strikes, prophylactic castle/pawn moves, central development)
112
+ const tacticalMoves = allMoves.filter((m) => m.isCheck || m.isCapture);
113
+ const quietMoves = allMoves.filter((m) => !m.isCheck && !m.isCapture);
114
+
115
+ const pool: AnnotatedMove[] = [];
116
+ if (tacticalMoves.length > 0) pool.push(...tacticalMoves.slice(0, 2));
117
+ if (quietMoves.length > 0) pool.push(...quietMoves.slice(0, Math.max(2, candidateLimit - pool.length)));
118
+
119
+ const candidates = pool.slice(0, candidateLimit);
120
+ const scoredCandidates: PersonaScoredMove[] = [];
121
+
122
+ // Evaluate each candidate move across atomic dimensions
123
+ for (const move of candidates) {
124
+ const state = {
125
+ candidate_move: {
126
+ san: move.san,
127
+ description: move.description,
128
+ is_capture: move.isCapture,
129
+ is_check: move.isCheck,
130
+ },
131
+ board: {
132
+ turn: engine.turn(),
133
+ fen: engine.fen(),
134
+ material: engine.getMaterialBalance().description,
135
+ },
136
+ };
137
+
138
+ const response = await this.client.systemOne({
139
+ state,
140
+ questions: {
141
+ aggression: score(
142
+ "How aggressive and attacking is `candidate_move.san`?",
143
+ AGGRESSION_RUBRIC
144
+ ),
145
+ prophylaxis: score(
146
+ "How well does `candidate_move.san` ensure safety and stifle opponent counterplay?",
147
+ PROPHYLAXIS_RUBRIC
148
+ ),
149
+ complexity: score(
150
+ "How much tactical tension and complexity does `candidate_move.san` inject into the board?",
151
+ COMPLEXITY_RUBRIC
152
+ ),
153
+ material_greed: noul(
154
+ "Is the move `candidate_move.san` primarily motivated by winning or defending material?"
155
+ ),
156
+ },
157
+ });
158
+
159
+ const dimScores: DimensionScores = {
160
+ aggression: response.answers.aggression.score,
161
+ prophylaxis: response.answers.prophylaxis.score,
162
+ complexity: response.answers.complexity.score,
163
+ materialGreed: response.answers.material_greed.noul,
164
+ };
165
+
166
+ // Composite scoring formula in code
167
+ const compositeScore =
168
+ persona.weights.aggression * dimScores.aggression +
169
+ persona.weights.prophylaxis * dimScores.prophylaxis +
170
+ persona.weights.complexity * dimScores.complexity +
171
+ persona.weights.materialGreed * dimScores.materialGreed;
172
+
173
+ scoredCandidates.push({
174
+ move,
175
+ compositeScore: Math.round(compositeScore * 1000) / 1000,
176
+ dimensionScores: dimScores,
177
+ });
178
+ }
179
+
180
+ // Sort by composite score descending
181
+ scoredCandidates.sort((a, b) => b.compositeScore - a.compositeScore);
182
+
183
+ const best = scoredCandidates[0]!;
184
+ const rationale = `${persona.name} (${persona.title}) selected ${best.move.san} with composite score ${best.compositeScore} (Aggression: ${best.dimensionScores.aggression.toFixed(1)}, Prophylaxis: ${best.dimensionScores.prophylaxis.toFixed(1)}, Complexity: ${best.dimensionScores.complexity.toFixed(1)}, Greed: ${best.dimensionScores.materialGreed.toFixed(2)}).`;
185
+
186
+ return {
187
+ persona,
188
+ selectedMove: best.move,
189
+ rankedCandidates: scoredCandidates,
190
+ rationale,
191
+ };
192
+ }
193
+ }