Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add multiplayer card table piece (table.mjs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+275
+275
system/public/aesthetic.computer/disks/table.mjs
··· 1 + // Table, 2026.03.30 2 + // A multiplayer card table with a shared deck. 3 + // Shuffle, draw, see who's seated. 4 + 5 + const SUITS = ["hearts", "diamonds", "clubs", "spades"]; 6 + const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]; 7 + const SYM = { hearts: "\u2665", diamonds: "\u2666", clubs: "\u2663", spades: "\u2660" }; 8 + const RED = [220, 60, 80]; 9 + const BLK = [40, 40, 50]; 10 + 11 + const CARD_W = 28; 12 + const CARD_H = 38; 13 + const CARD_GAP = 2; 14 + const BTN_W = 36; 15 + const BTN_H = 13; 16 + 17 + // Full 52-card reference 18 + const ALL = []; 19 + for (const s of SUITS) for (const r of RANKS) ALL.push({ rank: r, suit: s }); 20 + 21 + // -- State -- 22 + let server; 23 + let myHandle = "guest"; 24 + let myId = null; 25 + let players = {}; // id -> { handle, handSize } 26 + let deck = []; // indices into ALL 27 + let myHand = []; // indices I hold 28 + let deckSeed = 0; 29 + let sw = 0, sh = 0; 30 + let hoverCard = -1; 31 + let shuffleBtn, drawBtn; 32 + 33 + // Seeded shuffle so all clients get same order from same seed 34 + function seededShuffle(arr, seed) { 35 + const a = [...arr]; 36 + let s = seed; 37 + for (let i = a.length - 1; i > 0; i--) { 38 + s = (s * 1664525 + 1013904223) & 0xffffffff; 39 + const j = (s >>> 0) % (i + 1); 40 + [a[i], a[j]] = [a[j], a[i]]; 41 + } 42 + return a; 43 + } 44 + 45 + function doShuffle(seed) { 46 + deckSeed = seed; 47 + deck = seededShuffle(Array.from({ length: 52 }, (_, i) => i), seed); 48 + myHand = []; 49 + for (const p of Object.values(players)) p.handSize = 0; 50 + } 51 + 52 + function btnHit(btn, x, y) { 53 + return btn && x >= btn.x && x < btn.x + btn.w && y >= btn.y && y < btn.y + btn.h; 54 + } 55 + 56 + function boot({ wipe, screen, net: { socket }, handle }) { 57 + sw = screen.width; 58 + sh = screen.height; 59 + myHandle = handle?.() || "guest_" + Math.floor(Math.random() * 9999); 60 + doShuffle(Date.now()); 61 + 62 + server = socket((id, type, content) => { 63 + if (type.startsWith("connected")) { 64 + myId = id; 65 + server.send("cards:join", { handle: myHandle, seed: deckSeed }); 66 + return; 67 + } 68 + 69 + if (type === "left") { 70 + delete players[id]; 71 + return; 72 + } 73 + 74 + const msg = typeof content === "string" ? JSON.parse(content) : content; 75 + 76 + if (type === "cards:join") { 77 + players[id] = { handle: msg.handle, handSize: 0 }; 78 + // Tell newcomer current deck state 79 + server.send("cards:sync", { 80 + handle: myHandle, 81 + seed: deckSeed, 82 + deckLen: deck.length, 83 + handSize: myHand.length, 84 + }); 85 + } 86 + 87 + if (type === "cards:sync") { 88 + if (!players[id]) players[id] = { handle: msg.handle, handSize: 0 }; 89 + players[id].handSize = msg.handSize || 0; 90 + } 91 + 92 + if (type === "cards:shuffle") { 93 + doShuffle(msg.seed); 94 + } 95 + 96 + if (type === "cards:draw") { 97 + // Another player drew — pop from shared deck 98 + if (deck.length > 0) deck.pop(); 99 + if (!players[id]) players[id] = { handle: msg.handle, handSize: 0 }; 100 + players[id].handSize = msg.handSize; 101 + } 102 + 103 + if (type === "cards:discard") { 104 + if (!players[id]) players[id] = { handle: msg.handle, handSize: 0 }; 105 + players[id].handSize = msg.handSize; 106 + } 107 + }); 108 + 109 + wipe(245, 240, 230); 110 + } 111 + 112 + function act({ event: e, screen }) { 113 + sw = screen.width; 114 + sh = screen.height; 115 + 116 + const deckX = Math.floor(sw / 2 - CARD_W / 2); 117 + const deckY = Math.floor(sh / 2 - CARD_H / 2 - 14); 118 + shuffleBtn = { x: deckX - BTN_W - 4, y: deckY + CARD_H + 6, w: BTN_W, h: BTN_H }; 119 + drawBtn = { x: deckX + CARD_W + 4, y: deckY + CARD_H + 6, w: BTN_W, h: BTN_H }; 120 + 121 + // Hover cards in hand 122 + hoverCard = -1; 123 + if (e.is("move") || e.is("draw")) { 124 + const handY = sh - CARD_H - 10; 125 + const totalW = myHand.length * (CARD_W + CARD_GAP) - CARD_GAP; 126 + const startX = Math.floor(sw / 2 - totalW / 2); 127 + for (let i = myHand.length - 1; i >= 0; i--) { 128 + const cx = startX + i * (CARD_W + CARD_GAP); 129 + if (e.x >= cx && e.x < cx + CARD_W && e.y >= handY - 4 && e.y < handY + CARD_H) { 130 + hoverCard = i; 131 + break; 132 + } 133 + } 134 + } 135 + 136 + if (e.is("touch")) { 137 + if (btnHit(shuffleBtn, e.x, e.y)) { 138 + const seed = Date.now(); 139 + doShuffle(seed); 140 + server?.send("cards:shuffle", { seed, handle: myHandle }); 141 + return; 142 + } 143 + 144 + if (btnHit(drawBtn, e.x, e.y) && deck.length > 0) { 145 + const idx = deck.pop(); 146 + myHand.push(idx); 147 + server?.send("cards:draw", { handle: myHandle, handSize: myHand.length }); 148 + return; 149 + } 150 + 151 + // Tap card in hand to discard 152 + if (hoverCard >= 0) { 153 + myHand.splice(hoverCard, 1); 154 + server?.send("cards:discard", { handle: myHandle, handSize: myHand.length }); 155 + hoverCard = -1; 156 + } 157 + } 158 + } 159 + 160 + function paint({ wipe, ink, box, write, screen }) { 161 + sw = screen.width; 162 + sh = screen.height; 163 + wipe(245, 240, 230); 164 + 165 + // Felt area 166 + const pad = 10; 167 + ink(236, 230, 218).box(pad, pad, sw - pad * 2, sh - pad * 2); 168 + 169 + // Title 170 + ink(90, 80, 70).write("table", { x: 4, y: 4 }); 171 + 172 + // Players list (top right) 173 + const pList = [myHandle + " (you) " + myHand.length]; 174 + for (const p of Object.values(players)) { 175 + pList.push(p.handle + " " + (p.handSize || 0)); 176 + } 177 + let py = 4; 178 + for (const h of pList) { 179 + ink(150, 140, 130).write(h, { x: sw - h.length * 6 - 4, y: py }); 180 + py += 10; 181 + } 182 + 183 + // -- Deck (center) -- 184 + const deckX = Math.floor(sw / 2 - CARD_W / 2); 185 + const deckY = Math.floor(sh / 2 - CARD_H / 2 - 14); 186 + 187 + if (deck.length > 0) { 188 + // Stack shadow 189 + const n = Math.min(deck.length, 3); 190 + for (let i = n - 1; i >= 0; i--) { 191 + ink(205, 200, 190).box(deckX + i, deckY - i, CARD_W, CARD_H); 192 + ink(180, 175, 165).box(deckX + i, deckY - i, CARD_W, CARD_H, "outline"); 193 + } 194 + // Face-down pattern 195 + ink(130, 100, 150).box(deckX + 2, deckY + 2, CARD_W - 4, CARD_H - 4); 196 + ink(150, 120, 170).box(deckX + 4, deckY + 4, CARD_W - 8, CARD_H - 8, "outline"); 197 + 198 + const ct = "" + deck.length; 199 + ink(110, 100, 90).write(ct, { 200 + x: deckX + Math.floor(CARD_W / 2 - ct.length * 3), 201 + y: deckY + CARD_H + 2, 202 + }); 203 + } else { 204 + ink(225, 220, 210).box(deckX, deckY, CARD_W, CARD_H, "outline"); 205 + ink(205, 200, 190).write("--", { 206 + x: deckX + Math.floor(CARD_W / 2 - 6), 207 + y: deckY + Math.floor(CARD_H / 2 - 3), 208 + }); 209 + } 210 + 211 + // -- Buttons -- 212 + paintBtn(ink, box, write, shuffleBtn, "shuf", true); 213 + paintBtn(ink, box, write, drawBtn, "draw", deck.length > 0); 214 + 215 + // -- My hand -- 216 + if (myHand.length > 0) { 217 + const handY = sh - CARD_H - 10; 218 + const totalW = myHand.length * (CARD_W + CARD_GAP) - CARD_GAP; 219 + const startX = Math.floor(sw / 2 - totalW / 2); 220 + 221 + for (let i = 0; i < myHand.length; i++) { 222 + const c = ALL[myHand[i]]; 223 + const cx = startX + i * (CARD_W + CARD_GAP); 224 + const cy = hoverCard === i ? handY - 4 : handY; 225 + const col = c.suit === "hearts" || c.suit === "diamonds" ? RED : BLK; 226 + 227 + // Card bg 228 + ink(255, 253, 249).box(cx, cy, CARD_W, CARD_H); 229 + ink(185, 180, 175).box(cx, cy, CARD_W, CARD_H, "outline"); 230 + 231 + // Rank top-left 232 + ink(...col).write(c.rank, { x: cx + 2, y: cy + 2 }); 233 + // Suit center 234 + ink(...col).write(SYM[c.suit], { 235 + x: cx + Math.floor(CARD_W / 2 - 3), 236 + y: cy + Math.floor(CARD_H / 2 - 3), 237 + }); 238 + // Rank bottom-right 239 + ink(...col).write(c.rank, { 240 + x: cx + CARD_W - c.rank.length * 6 - 2, 241 + y: cy + CARD_H - 9, 242 + }); 243 + } 244 + } 245 + 246 + // Hint 247 + const hint = myHand.length > 0 ? "tap card to discard" : "draw from the deck"; 248 + ink(195, 185, 175).write(hint, { 249 + x: Math.floor(sw / 2 - hint.length * 3), 250 + y: sh - 6, 251 + }); 252 + } 253 + 254 + function paintBtn(ink, box, write, btn, label, active) { 255 + if (!btn) return; 256 + if (active) { 257 + ink(215, 208, 198).box(btn.x, btn.y, btn.w, btn.h); 258 + ink(170, 163, 153).box(btn.x, btn.y, btn.w, btn.h, "outline"); 259 + ink(100, 90, 80).write(label, { x: btn.x + 6, y: btn.y + 3 }); 260 + } else { 261 + ink(232, 228, 222).box(btn.x, btn.y, btn.w, btn.h); 262 + ink(210, 205, 200).box(btn.x, btn.y, btn.w, btn.h, "outline"); 263 + ink(205, 200, 195).write(label, { x: btn.x + 6, y: btn.y + 3 }); 264 + } 265 + } 266 + 267 + function meta() { 268 + return { 269 + title: "Table", 270 + desc: "Multiplayer card table. Shuffle, draw, and see who's at the table.", 271 + }; 272 + } 273 + 274 + export { boot, act, paint, meta }; 275 + export const desc = "Multiplayer card table.";