1
0

5 Commits 65509ca33e ... ea6104d0ea

Autor SHA1 Mensagem Data
  Oleg Panashchenko ea6104d0ea fix: keep unknown word on screen instead of wiping it 2 semanas atrás
  Oleg Panashchenko 8813d433d4 feat: add deploy script (rsync to helg.com) 2 semanas atrás
  Oleg Panashchenko 6619543932 fix: make dist/ a complete self-contained deployable 2 semanas atrás
  Oleg Panashchenko 8b473443fc feat: add build script for deployable dist/ target 2 semanas atrás
  Oleg Panashchenko 30f54d2866 feat: updated word-bank with extended word list 2 semanas atrás

+ 11 - 1
.claude/settings.local.json

@@ -30,7 +30,17 @@
       "Bash(echo \"solver: $?\")",
       "Bash(fuser -k 3001/tcp)",
       "Bash(curl -s -X POST http://localhost:3001/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"style\",\"tryNo\":1}')",
-      "Bash(curl -s -X POST http://localhost:3001/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"crane\",\"tryNo\":6}')"
+      "Bash(curl -s -X POST http://localhost:3001/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"crane\",\"tryNo\":6}')",
+      "Bash(npm run *)",
+      "Bash(PORT=3001 STATIC_DIR=dist/client npx tsx server/src/index.ts)",
+      "Bash(curl -s http://localhost:3001/)",
+      "Bash(curl -s http://localhost:3001/api/daily)",
+      "Bash(npm install *)",
+      "Bash(STATIC_DIR=client PORT=3001 node --import tsx server/src/index.ts)",
+      "Bash(STATIC_DIR=client PORT=3002 node --import tsx server/src/index.ts)",
+      "Bash(curl -s http://localhost:3002/api/daily)",
+      "Bash(curl -s http://localhost:3002/)",
+      "Bash(curl -s -X POST http://localhost:3002/api/guess -H 'Content-Type: application/json' -d '{\"wordId\":4,\"guess\":\"style\",\"tryNo\":1}')"
     ]
   }
 }

+ 2 - 2
client/src/App.tsx

@@ -54,8 +54,8 @@ export default function App() {
 
   const handleGuess = useCallback(
     async (guess: string) => {
-      if (wordId === null) return;
-      await submitGuess(wordId, guess);
+      if (wordId === null) return false;
+      return submitGuess(wordId, guess);
     },
     [wordId, submitGuess]
   );

+ 4 - 3
client/src/hooks/useGame.ts

@@ -10,7 +10,7 @@ interface UseGameReturn {
   lastDifficulty: number | null;
   revealedWord: string | null;
   replays: SolverReplay[] | null;
-  submitGuess: (wordId: number, guess: string) => Promise<void>;
+  submitGuess: (wordId: number, guess: string) => Promise<boolean>;
   dismissMessage: () => void;
   resetForNewGame: () => void;
 }
@@ -46,14 +46,14 @@ export function useGame(initialGuesses: GuessEntry[]): UseGameReturn {
     if (!res.ok) {
       const body = await res.json().catch(() => ({}));
       setMessage((body as { error?: string }).error ?? `HTTP ${res.status}`);
-      return;
+      return false;
     }
 
     const data: GuessResponse = await res.json();
 
     if (!data.valid) {
       setMessage('Unknown word');
-      return;
+      return false;
     }
 
     const entry: GuessEntry = { guess: lowerGuess, colors: data.colors! };
@@ -68,6 +68,7 @@ export function useGame(initialGuesses: GuessEntry[]): UseGameReturn {
     if (data.replays) {
       setReplays(data.replays);
     }
+    return true;
   }, [guesses.length]);
 
   return { guesses, gameStatus, message, lastDifficulty, revealedWord, replays, submitGuess, dismissMessage, resetForNewGame };

+ 7 - 14
client/src/screens/Screen2Game.tsx

@@ -7,7 +7,7 @@ interface Screen2GameProps {
   guesses: GuessEntry[];
   letterColors: Record<string, string | undefined>;
   message: string | null;
-  onGuess: (guess: string) => Promise<void>;
+  onGuess: (guess: string) => Promise<boolean>;
   onDismissMessage: () => void;
   gameOver: boolean;
 }
@@ -24,8 +24,10 @@ export function Screen2Game({ guesses, letterColors, message, onGuess, onDismiss
 
   const submitCurrent = useCallback(async () => {
     if (currentGuess.length !== 5 || gameOver) return;
-    await onGuess(currentGuess);
-    setCurrentGuess(''); // clear on valid submission
+    const accepted = await onGuess(currentGuess);
+    if (accepted) {
+      setCurrentGuess(''); // only clear when guess was valid
+    }
   }, [currentGuess, onGuess, gameOver]);
 
   // Physical keyboard handler
@@ -38,12 +40,7 @@ export function Screen2Game({ guesses, letterColors, message, onGuess, onDismiss
         return;
       }
       if (e.key === 'Backspace') {
-        // If there's a message (invalid word), clear the whole row
-        if (message) {
-          onDismissMessage();
-          setCurrentGuess('');
-          return;
-        }
+        if (message) onDismissMessage();
         setCurrentGuess((prev) => prev.slice(0, -1));
         return;
       }
@@ -63,11 +60,7 @@ export function Screen2Game({ guesses, letterColors, message, onGuess, onDismiss
       if (key === 'Enter') {
         submitCurrent();
       } else if (key === 'Backspace') {
-        if (message) {
-          onDismissMessage();
-          setCurrentGuess('');
-          return;
-        }
+        if (message) onDismissMessage();
         setCurrentGuess((prev) => prev.slice(0, -1));
       } else if (currentGuess.length < 5) {
         setCurrentGuess((prev) => prev + key.toLowerCase());

+ 184 - 2
data/word-bank.json

@@ -9242,7 +9242,189 @@
     {
       "id": 43,
       "word": "spire",
-      "difficulty": 3.0
+      "difficulty": 4,
+      "replays": [
+        {
+          "solver": "Entropy",
+          "steps": [
+            {
+              "guess": "raise",
+              "colors": [
+                "y",
+                "x",
+                "g",
+                "y",
+                "g"
+              ]
+            },
+            {
+              "guess": "shire",
+              "colors": [
+                "g",
+                "x",
+                "g",
+                "g",
+                "g"
+              ]
+            },
+            {
+              "guess": "spire",
+              "colors": [
+                "g",
+                "g",
+                "g",
+                "g",
+                "g"
+              ]
+            }
+          ]
+        },
+        {
+          "solver": "Frequency",
+          "steps": [
+            {
+              "guess": "irate",
+              "colors": [
+                "y",
+                "y",
+                "x",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "dirge",
+              "colors": [
+                "x",
+                "y",
+                "y",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "shire",
+              "colors": [
+                "g",
+                "x",
+                "g",
+                "g",
+                "g"
+              ]
+            },
+            {
+              "guess": "spire",
+              "colors": [
+                "g",
+                "g",
+                "g",
+                "g",
+                "g"
+              ]
+            }
+          ]
+        },
+        {
+          "solver": "Vowel First",
+          "steps": [
+            {
+              "guess": "audio",
+              "colors": [
+                "x",
+                "x",
+                "x",
+                "y",
+                "x"
+              ]
+            },
+            {
+              "guess": "elite",
+              "colors": [
+                "x",
+                "x",
+                "g",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "brine",
+              "colors": [
+                "x",
+                "y",
+                "g",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "shire",
+              "colors": [
+                "g",
+                "x",
+                "g",
+                "g",
+                "g"
+              ]
+            },
+            {
+              "guess": "spire",
+              "colors": [
+                "g",
+                "g",
+                "g",
+                "g",
+                "g"
+              ]
+            }
+          ]
+        },
+        {
+          "solver": "Greedy",
+          "steps": [
+            {
+              "guess": "crane",
+              "colors": [
+                "x",
+                "y",
+                "x",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "dirge",
+              "colors": [
+                "x",
+                "y",
+                "y",
+                "x",
+                "g"
+              ]
+            },
+            {
+              "guess": "shire",
+              "colors": [
+                "g",
+                "x",
+                "g",
+                "g",
+                "g"
+              ]
+            },
+            {
+              "guess": "spire",
+              "colors": [
+                "g",
+                "g",
+                "g",
+                "g",
+                "g"
+              ]
+            }
+          ]
+        }
+      ]
     }
   ]
-}
+}

+ 12 - 0
dist-package.json

@@ -0,0 +1,12 @@
+{
+  "name": "wordle-deploy",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "start": "node --import tsx server/src/index.ts"
+  },
+  "dependencies": {
+    "express": "^5.0.0",
+    "tsx": "^4.0.0"
+  }
+}

+ 10 - 1
package.json

@@ -1,5 +1,14 @@
 {
   "name": "wordle",
   "private": true,
-  "workspaces": ["client", "server", "shared", "solver"]
+  "workspaces": ["client", "server", "shared", "solver"],
+  "scripts": {
+    "build": "npm run build:client && npm run build:bundle",
+    "build:client": "cd client && npx vite build",
+    "build:bundle": "rm -rf dist && mkdir -p dist/server/src/routes dist/shared/src dist/client dist/data && cp -r client/dist/* dist/client/ && cp -r server/src/* dist/server/src/ && cp -r shared/src/* dist/shared/src/ && cp data/word-bank.json dist/data/ && cp dist-package.json dist/package.json && find dist/server/src -name '*.ts' -exec sed -i \"s|@wordle/shared|../../shared/src/index.js|g\" {} +",
+    "start": "node --import tsx server/src/index.ts",
+    "dev": "node --import tsx server/src/index.ts",
+    "solve": "cd solver && npm run solve",
+    "deploy": "npm run build && rsync -avz dist/ helg.com:/var/www/wordle/"
+  }
 }

+ 3 - 1
server/src/index.ts

@@ -22,7 +22,9 @@ app.use('/api/guess', createGuessRouter(wordBank));
 app.use('/api/play-again', createPlayAgainRouter(wordBank));
 
 // Serve SPA in production
-const clientDist = resolve(__dirname, '../../client/dist');
+const clientDist = process.env.STATIC_DIR
+  ? resolve(process.env.STATIC_DIR)
+  : resolve(__dirname, '../../client/dist');
 app.use(express.static(clientDist));
 app.get('/{*path}', (_req, res) => {
   res.sendFile(resolve(clientDist, 'index.html'));