Преглед изворни кода

refactor: compute solver replay colors client-side instead of storing in word-bank

Removed 'colors' from SolverStep type and both word-bank JSON files.
Client now computes feedback colors at render time using the same
green/yellow/gray algorithm as the server. Reduces data file size
and API response payload.
Oleg Panashchenko пре 1 недеља
родитељ
комит
9141e1e3b5
5 измењених фајлова са 296 додато и 1362 уклоњено
  1. 6 1
      client/src/App.tsx
  2. 59 23
      client/src/screens/Screen3Results.tsx
  3. 115 668
      data/word-bank-ru.json
  4. 115 668
      data/word-bank.json
  5. 1 2
      shared/src/word-bank.ts

+ 6 - 1
client/src/App.tsx

@@ -128,11 +128,16 @@ export default function App() {
   }
 
   if (screen === 'results') {
+    // On win, the last guess IS the target word. On loss, the server reveals it.
+    const targetWord =
+      revealedWord ??
+      (gameStatus === 'won' ? guesses[guesses.length - 1]?.guess ?? null : null);
+
     return (
       <Screen3Results
         won={gameStatus === 'won'}
         attemptCount={guesses.length}
-        revealedWord={revealedWord}
+        revealedWord={targetWord}
         stats={state.stats}
         replays={replays ?? undefined}
         onPlayAgain={handlePlayAgain}

+ 59 - 23
client/src/screens/Screen3Results.tsx

@@ -13,32 +13,68 @@ interface Screen3ResultsProps {
 
 const MINI_COLORS: Record<string, string> = { g: '#6aaa64', y: '#c9b458', x: '#787c7e' };
 
-function MiniBoard({ replay }: { replay: SolverReplay }) {
+/** Compute Wordle feedback colors for a guess against the target word (same algorithm as server). */
+function computeFeedback(target: string, guess: string): string[] {
+  const targetUpper = target.toLowerCase();
+  const guessUpper = guess.toLowerCase();
+
+  const targetCounts: Record<string, number> = {};
+  for (const ch of targetUpper) {
+    targetCounts[ch] = (targetCounts[ch] ?? 0) + 1;
+  }
+
+  const result: string[] = Array(5).fill('x');
+
+  // Pass 1: greens
+  for (let i = 0; i < 5; i++) {
+    if (guessUpper[i] === targetUpper[i]) {
+      result[i] = 'g';
+      targetCounts[guessUpper[i]]--;
+    }
+  }
+
+  // Pass 2: yellows
+  for (let i = 0; i < 5; i++) {
+    if (result[i] === 'g') continue;
+    const ch = guessUpper[i];
+    if ((targetCounts[ch] ?? 0) > 0) {
+      result[i] = 'y';
+      targetCounts[ch]--;
+    }
+  }
+
+  return result;
+}
+
+function MiniBoard({ replay, targetWord }: { replay: SolverReplay; targetWord: string }) {
   return (
     <div style={{ marginBottom: '12px' }}>
       <div style={{ fontSize: '0.7rem', fontWeight: 'bold', color: '#555', marginBottom: '4px', textAlign: 'center' }}>
         {replay.solver}
       </div>
-      {replay.steps.map((step, rowIdx) => (
-        <div key={rowIdx} style={{ display: 'flex', gap: '2px', justifyContent: 'center', marginBottom: '2px' }}>
-          {step.guess.split('').map((letter, colIdx) => {
-            const c = step.colors[colIdx];
-            return (
-              <div key={colIdx} style={{
-                width: '20px', height: '20px',
-                backgroundColor: MINI_COLORS[c] ?? '#d3d6da',
-                color: '#fff',
-                display: 'flex', alignItems: 'center', justifyContent: 'center',
-                fontSize: '0.55rem', fontWeight: 'bold',
-                textTransform: 'uppercase', fontFamily: 'monospace',
-                borderRadius: '2px',
-              }}>
-                {letter}
-              </div>
-            );
-          })}
-        </div>
-      ))}
+      {replay.steps.map((step, rowIdx) => {
+        const colors = computeFeedback(targetWord, step.guess);
+        return (
+          <div key={rowIdx} style={{ display: 'flex', gap: '2px', justifyContent: 'center', marginBottom: '2px' }}>
+            {step.guess.split('').map((letter, colIdx) => {
+              const c = colors[colIdx];
+              return (
+                <div key={colIdx} style={{
+                  width: '20px', height: '20px',
+                  backgroundColor: MINI_COLORS[c] ?? '#d3d6da',
+                  color: '#fff',
+                  display: 'flex', alignItems: 'center', justifyContent: 'center',
+                  fontSize: '0.55rem', fontWeight: 'bold',
+                  textTransform: 'uppercase', fontFamily: 'monospace',
+                  borderRadius: '2px',
+                }}>
+                  {letter}
+                </div>
+              );
+            })}
+          </div>
+        );
+      })}
     </div>
   );
 }
@@ -92,14 +128,14 @@ export function Screen3Results({ won, attemptCount, revealedWord, stats, replays
         <Histogram stats={stats} />
       </div>
 
-      {replays && replays.length > 0 && (
+      {replays && replays.length > 0 && revealedWord && (
         <div style={{ margin: '24px 0' }}>
           <p style={{ fontSize: '0.85rem', color: '#787c7e', marginBottom: '12px' }}>
             {r.solverCaption}
           </p>
           <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', maxWidth: '320px', margin: '0 auto' }}>
             {replays.map((rp) => (
-              <MiniBoard key={rp.solver} replay={rp} />
+              <MiniBoard key={rp.solver} replay={rp} targetWord={revealedWord} />
             ))}
           </div>
         </div>

Разлика између датотеке није приказан због своје велике величине
+ 115 - 668
data/word-bank-ru.json


Разлика између датотеке није приказан због своје велике величине
+ 115 - 668
data/word-bank.json


+ 1 - 2
shared/src/word-bank.ts

@@ -1,7 +1,6 @@
-/** A single solver replay step */
+/** A single solver replay step — colors are computed client-side from the known target word */
 export interface SolverStep {
   guess: string;
-  colors: string[];
 }
 
 /** Replay from one solver algorithm */

Неке датотеке нису приказане због велике количине промена