code.pablomurad.com

emchess

Browser chess with emoji pieces against Stockfish, no server needed

feat: fix colors
author
Pablo Murad
date
commit
e3439f4fd1351a66738e364bb3c2a16038ca6e6a
parent
582b0387fa0b9b6f7b51a8a13c4fee17764ffb39
changes
4 files +35 −33
@@ -1,18 +1,10 @@+// A real same-origin URL (not a blob), so Stockfish resolves its .wasm next to itself.
+// The server must send the .wasm as application/wasm.
 const ENGINE_URL = new URL(`${import.meta.env.BASE_URL}stockfish/stockfish-18-lite-single.js`, location.href).href;
-const WASM_URL = new URL(`${import.meta.env.BASE_URL}stockfish/stockfish-18-lite-single.wasm`, location.href).href;
-// A tiny worker that fetches the wasm as a plain binary and hands it to Stockfish
-// through Module.wasmBinary. That path ignores the response's MIME type, so a server
-// that serves .wasm as text/plain (a very common misconfig) can't break the engine.
-const BOOTSTRAP = `self.onmessage=function(e){var d=e.data||{};if(!d.__boot)return;` +
-    `fetch(d.wasmUrl).then(function(r){return r.arrayBuffer();}).then(function(buf){` +
-    `self.Module={wasmBinary:buf,locateFile:function(p){return p.slice(-5)==='.wasm'?d.wasmUrl:p;}};` +
-    `try{importScripts(d.jsUrl);postMessage('__ready');}catch(err){postMessage('__err '+err);}` +
-    `}).catch(function(err){postMessage('__err '+err);});};`;
 export class Engine {
     constructor() {
         this.listeners = [];
-        const bootUrl = URL.createObjectURL(new Blob([BOOTSTRAP], { type: "text/javascript" }));
-        this.worker = new Worker(bootUrl);
+        this.worker = new Worker(ENGINE_URL);
         this.worker.onmessage = (e) => {
             const line = typeof e.data === "string" ? e.data : String(e.data?.data ?? "");
             for (const fn of this.listeners)
@@ -44,8 +36,6 @@ }         });
     }
     async boot() {
-        this.worker.postMessage({ __boot: true, jsUrl: ENGINE_URL, wasmUrl: WASM_URL });
-        await this.waitFor("__ready", 20000);
         this.send("uci");
         await this.waitFor("uciok", 20000);
         this.send("isready");
@@ -1,16 +1,8 @@ import type { Personality } from "./personalities";
 
+// A real same-origin URL (not a blob), so Stockfish resolves its .wasm next to itself.
+// The server must send the .wasm as application/wasm.
 const ENGINE_URL = new URL(`${import.meta.env.BASE_URL}stockfish/stockfish-18-lite-single.js`, location.href).href;
-const WASM_URL = new URL(`${import.meta.env.BASE_URL}stockfish/stockfish-18-lite-single.wasm`, location.href).href;
-
-// A tiny worker that fetches the wasm as a plain binary and hands it to Stockfish
-// through Module.wasmBinary. That path ignores the response's MIME type, so a server
-// that serves .wasm as text/plain (a very common misconfig) can't break the engine.
-const BOOTSTRAP = `self.onmessage=function(e){var d=e.data||{};if(!d.__boot)return;` +
-  `fetch(d.wasmUrl).then(function(r){return r.arrayBuffer();}).then(function(buf){` +
-  `self.Module={wasmBinary:buf,locateFile:function(p){return p.slice(-5)==='.wasm'?d.wasmUrl:p;}};` +
-  `try{importScripts(d.jsUrl);postMessage('__ready');}catch(err){postMessage('__err '+err);}` +
-  `}).catch(function(err){postMessage('__err '+err);});};`;
 
 type Line = string;
 
@@ -20,8 +12,7 @@ private ready: Promise<void>;   private listeners: ((line: Line) => void)[] = [];
 
   constructor() {
-    const bootUrl = URL.createObjectURL(new Blob([BOOTSTRAP], { type: "text/javascript" }));
-    this.worker = new Worker(bootUrl);
+    this.worker = new Worker(ENGINE_URL);
     this.worker.onmessage = (e) => {
       const line = typeof e.data === "string" ? e.data : String(e.data?.data ?? "");
       for (const fn of this.listeners) fn(line);
@@ -54,8 +45,6 @@ });   }
 
   private async boot() {
-    this.worker.postMessage({ __boot: true, jsUrl: ENGINE_URL, wasmUrl: WASM_URL });
-    await this.waitFor("__ready", 20000);
     this.send("uci");
     await this.waitFor("uciok", 20000);
     this.send("isready");
@@ -216,9 +216,28 @@ transition: transform .26s cubic-bezier(.2,.7,.25,1);   will-change: transform;
 }
 .piece .glyph { position: relative; z-index: 2; }
-.piece::before { content: ""; position: absolute; bottom: 8%; width: 64%; height: 18%; border-radius: 50%; z-index: 1; }
-.piece.w::before { background: rgba(252,249,240,.92); box-shadow: inset 0 0 0 1px rgba(0,0,0,.3); }
-.piece.b::before { background: rgba(30,27,20,.85); box-shadow: inset 0 0 0 1px rgba(255,255,255,.28); }
+/* Emoji share one glyph per type, so the light and dark armies are told apart by the
+   token each piece rides on, not by the emoji's own colors. */
+/* A grounding shadow plus a hairline light rim, so a mid-tone emoji (a grey wolf, say)
+   still reads on the dark army's token without being darkened into it. */
+.piece:not(.traditional) .glyph {
+  filter: drop-shadow(0 1px 1px rgba(0,0,0,.4)) drop-shadow(0 0 .5px rgba(255,255,255,.55));
+}
+.piece::before {
+  content: "";
+  position: absolute;
+  inset: 9%;
+  border-radius: 50%;
+  z-index: 1;
+}
+.piece.w::before {
+  background: radial-gradient(circle at 38% 32%, #fffdf6, #e7dcbe 78%);
+  box-shadow: 0 2px 4px rgba(0,0,0,.22), inset 0 0 0 2px rgba(90,70,28,.5);
+}
+.piece.b::before {
+  background: radial-gradient(circle at 38% 32%, #3b362a, #14110b 78%);
+  box-shadow: 0 2px 4px rgba(0,0,0,.38), inset 0 0 0 2px rgba(243,238,220,.5);
+}
 .piece.traditional { font-family: var(--mono); font-size: calc(var(--sq) * .8); }
 .piece.traditional.w { color: #fbf7ec; text-shadow: 0 0 1px #000, 0 1px 1px rgba(0,0,0,.45); }
 .piece.traditional.b { color: #141109; }
@@ -230,11 +249,11 @@ @keyframes hop { 0% { transform: translateY(0); } 45% { transform: translateY(-24%); } 100% { transform: translateY(0); } } .piece.threat::after {
   content: "";
   position: absolute;
-  inset: 7%;
+  inset: 3%;
   border: 2px solid var(--check);
   border-radius: 50%;
-  opacity: .85;
-  z-index: 0;
+  opacity: .9;
+  z-index: 3;
   pointer-events: none;
 }
 
@@ -14,6 +14,10 @@ workbox: {         globPatterns: ["**/*.{js,css,html,wasm,woff,woff2,svg}"],
         // The Stockfish wasm is ~7MB, so raise the precache size limit.
         maximumFileSizeToCacheInBytes: 9 * 1024 * 1024,
+        // Take over at once and drop old caches, so a bad old cache can't linger.
+        skipWaiting: true,
+        clientsClaim: true,
+        cleanupOutdatedCaches: true,
       },
       manifest: {
         name: "Emchess",