Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

table: draggable cards on shared surface with multiplayer sync

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

+342 -132
+342 -132
system/public/aesthetic.computer/disks/table.mjs
··· 1 1 // Table, 2026.03.30 2 - // A multiplayer card table with a shared deck. 3 - // Shuffle, draw, see who's seated. 2 + // A multiplayer card table — drag cards on a shared surface. 3 + // Fixed table geography (like a pool table). One card per player at a time. 4 4 5 5 const SUITS = ["hearts", "diamonds", "clubs", "spades"]; 6 6 const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]; ··· 10 10 11 11 const CARD_W = 28; 12 12 const CARD_H = 38; 13 - const CARD_GAP = 2; 14 13 const BTN_W = 36; 15 14 const BTN_H = 13; 16 15 16 + // Table size (fixed geography, larger than screen) 17 + const TABLE_W = 480; 18 + const TABLE_H = 320; 19 + 17 20 // Full 52-card reference 18 21 const ALL = []; 19 22 for (const s of SUITS) for (const r of RANKS) ALL.push({ rank: r, suit: s }); 20 23 21 24 // -- State -- 22 - let server; 25 + let server, udpChannel; 23 26 let myHandle = "guest"; 24 27 let myId = null; 25 - let players = {}; // id -> { handle, handSize } 26 - let deck = []; // indices into ALL 27 - let myHand = []; // indices I hold 28 + let players = {}; // id -> { handle, dragging: cardIdx|null, dx, dy } 29 + let frameCount = 0; 30 + 31 + // Cards on the table: { idx, x, y, faceUp, dragger: handle|null } 32 + let tableCards = []; 33 + let deck = []; // remaining card indices 28 34 let deckSeed = 0; 35 + 36 + // Deck position on table (fixed) 37 + const DECK_X = Math.floor(TABLE_W / 2 - CARD_W / 2); 38 + const DECK_Y = Math.floor(TABLE_H / 2 - CARD_H / 2); 39 + 40 + // Camera (viewport offset into table) 41 + let camX = 0, camY = 0; 29 42 let sw = 0, sh = 0; 30 - let hoverCard = -1; 31 - let shuffleBtn, drawBtn; 32 43 33 - // Seeded shuffle so all clients get same order from same seed 44 + // Local drag state 45 + let dragging = null; // index into tableCards 46 + let dragOffX = 0, dragOffY = 0; 47 + let hoverIdx = -1; // index into tableCards 48 + 49 + // Button rects (screen space) 50 + let shuffleBtn = null, drawBtn = null; 51 + 52 + // Seeded shuffle 34 53 function seededShuffle(arr, seed) { 35 54 const a = [...arr]; 36 55 let s = seed; ··· 45 64 function doShuffle(seed) { 46 65 deckSeed = seed; 47 66 deck = seededShuffle(Array.from({ length: 52 }, (_, i) => i), seed); 48 - myHand = []; 49 - for (const p of Object.values(players)) p.handSize = 0; 67 + tableCards = []; 68 + dragging = null; 69 + hoverIdx = -1; 70 + } 71 + 72 + // Convert screen coords to table coords 73 + function toTable(sx, sy) { 74 + return { tx: sx + camX, ty: sy + camY }; 75 + } 76 + 77 + // Convert table coords to screen coords 78 + function toScreen(tx, ty) { 79 + return { sx: tx - camX, sy: ty - camY }; 50 80 } 51 81 52 82 function btnHit(btn, x, y) { 53 83 return btn && x >= btn.x && x < btn.x + btn.w && y >= btn.y && y < btn.y + btn.h; 54 84 } 55 85 56 - function boot({ wipe, screen, net: { socket }, handle }) { 86 + // Find topmost card at table position (last in array = top) 87 + function cardAtPos(tx, ty) { 88 + for (let i = tableCards.length - 1; i >= 0; i--) { 89 + const c = tableCards[i]; 90 + if (tx >= c.x && tx < c.x + CARD_W && ty >= c.y && ty < c.y + CARD_H) { 91 + return i; 92 + } 93 + } 94 + return -1; 95 + } 96 + 97 + // Bring card to top of stack 98 + function bringToTop(idx) { 99 + const card = tableCards.splice(idx, 1)[0]; 100 + tableCards.push(card); 101 + return tableCards.length - 1; 102 + } 103 + 104 + function boot({ wipe, screen, net: { socket, udp }, handle }) { 57 105 sw = screen.width; 58 106 sh = screen.height; 59 107 myHandle = handle?.() || "guest_" + Math.floor(Math.random() * 9999); 108 + 109 + // Center camera on table 110 + camX = Math.floor(TABLE_W / 2 - sw / 2); 111 + camY = Math.floor(TABLE_H / 2 - sh / 2); 112 + 60 113 doShuffle(Date.now()); 61 114 115 + // UDP for low-latency drag position sync 116 + udpChannel = udp((type, content) => { 117 + if (type === "table:drag") { 118 + const d = typeof content === "string" ? JSON.parse(content) : content; 119 + if (d.handle === myHandle) return; 120 + // Find card on table and update its position 121 + const tc = tableCards.find((c) => c.idx === d.cardIdx); 122 + if (tc) { 123 + tc.x = d.x; 124 + tc.y = d.y; 125 + tc.dragger = d.handle; 126 + } 127 + } 128 + }); 129 + 62 130 server = socket((id, type, content) => { 63 131 if (type.startsWith("connected")) { 64 132 myId = id; 65 - server.send("cards:join", { handle: myHandle, seed: deckSeed }); 133 + server.send("table:join", { handle: myHandle }); 66 134 return; 67 135 } 68 136 69 137 if (type === "left") { 138 + // Release any card they were dragging 139 + for (const c of tableCards) { 140 + if (players[id] && c.dragger === players[id].handle) { 141 + c.dragger = null; 142 + } 143 + } 70 144 delete players[id]; 71 145 return; 72 146 } 73 147 74 148 const msg = typeof content === "string" ? JSON.parse(content) : content; 75 149 76 - if (type === "cards:join") { 77 - players[id] = { handle: msg.handle, handSize: 0 }; 78 - // Tell newcomer current deck state 79 - server.send("cards:sync", { 150 + if (type === "table:join") { 151 + players[id] = { handle: msg.handle }; 152 + // Send full state to newcomer 153 + server.send("table:state", { 80 154 handle: myHandle, 81 155 seed: deckSeed, 82 156 deckLen: deck.length, 83 - handSize: myHand.length, 157 + cards: tableCards.map((c) => ({ 158 + idx: c.idx, x: c.x, y: c.y, faceUp: c.faceUp, dragger: c.dragger, 159 + })), 84 160 }); 85 161 } 86 162 87 - if (type === "cards:sync") { 88 - if (!players[id]) players[id] = { handle: msg.handle, handSize: 0 }; 89 - players[id].handSize = msg.handSize || 0; 163 + if (type === "table:state") { 164 + if (!players[id]) players[id] = { handle: msg.handle }; 165 + // Sync table state from existing player 166 + if (msg.cards && msg.cards.length > 0 && tableCards.length === 0) { 167 + // Only accept if we have no cards yet (fresh join) 168 + doShuffle(msg.seed); 169 + // Remove cards that are on the table from the deck 170 + for (const c of msg.cards) { 171 + const di = deck.indexOf(c.idx); 172 + if (di >= 0) deck.splice(di, 1); 173 + tableCards.push({ 174 + idx: c.idx, x: c.x, y: c.y, 175 + faceUp: c.faceUp, dragger: c.dragger, 176 + }); 177 + } 178 + } 90 179 } 91 180 92 - if (type === "cards:shuffle") { 181 + if (type === "table:shuffle") { 93 182 doShuffle(msg.seed); 94 183 } 95 184 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; 185 + if (type === "table:draw") { 186 + // Another player drew a card onto the table 187 + const di = deck.indexOf(msg.cardIdx); 188 + if (di >= 0) deck.splice(di, 1); 189 + tableCards.push({ 190 + idx: msg.cardIdx, x: msg.x, y: msg.y, 191 + faceUp: true, dragger: null, 192 + }); 193 + } 194 + 195 + if (type === "table:grab") { 196 + const tc = tableCards.find((c) => c.idx === msg.cardIdx); 197 + if (tc) { 198 + tc.dragger = msg.handle; 199 + // Bring to top 200 + const i = tableCards.indexOf(tc); 201 + if (i >= 0) bringToTop(i); 202 + } 203 + } 204 + 205 + if (type === "table:drop") { 206 + const tc = tableCards.find((c) => c.idx === msg.cardIdx); 207 + if (tc) { 208 + tc.x = msg.x; 209 + tc.y = msg.y; 210 + tc.dragger = null; 211 + } 101 212 } 102 213 103 - if (type === "cards:discard") { 104 - if (!players[id]) players[id] = { handle: msg.handle, handSize: 0 }; 105 - players[id].handSize = msg.handSize; 214 + if (type === "table:flip") { 215 + const tc = tableCards.find((c) => c.idx === msg.cardIdx); 216 + if (tc) tc.faceUp = !tc.faceUp; 106 217 } 107 218 }); 108 219 109 220 wipe(245, 240, 230); 110 221 } 111 222 223 + function sim() { 224 + frameCount++; 225 + 226 + // Send drag position via UDP every 2 frames 227 + if (dragging !== null && frameCount % 2 === 0 && udpChannel?.connected) { 228 + const c = tableCards[dragging]; 229 + if (c) { 230 + udpChannel.send("table:drag", { 231 + handle: myHandle, 232 + cardIdx: c.idx, 233 + x: c.x, 234 + y: c.y, 235 + }); 236 + } 237 + } 238 + } 239 + 112 240 function act({ event: e, screen }) { 113 241 sw = screen.width; 114 242 sh = screen.height; 115 243 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 }; 244 + // Button positions (screen space, near bottom-left) 245 + shuffleBtn = { x: 4, y: sh - BTN_H - 4, w: BTN_W, h: BTN_H }; 246 + drawBtn = { x: 4 + BTN_W + 4, y: sh - BTN_H - 4, w: BTN_W, h: BTN_H }; 120 247 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 - } 248 + // Hover detection 249 + if (e.is("move")) { 250 + const { tx, ty } = toTable(e.x, e.y); 251 + hoverIdx = cardAtPos(tx, ty); 134 252 } 135 253 254 + // Touch: start dragging a card or tap buttons 136 255 if (e.is("touch")) { 256 + // Check buttons first (screen space) 137 257 if (btnHit(shuffleBtn, e.x, e.y)) { 138 258 const seed = Date.now(); 139 259 doShuffle(seed); 140 - server?.send("cards:shuffle", { seed, handle: myHandle }); 260 + server?.send("table:shuffle", { seed, handle: myHandle }); 141 261 return; 142 262 } 143 263 144 264 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 }); 265 + const cardIdx = deck.pop(); 266 + // Place near deck position with slight random offset 267 + const ox = (Math.random() - 0.5) * 40; 268 + const oy = (Math.random() - 0.5) * 20 + CARD_H + 8; 269 + const x = DECK_X + ox; 270 + const y = DECK_Y + oy; 271 + tableCards.push({ idx: cardIdx, x, y, faceUp: true, dragger: null }); 272 + server?.send("table:draw", { 273 + handle: myHandle, cardIdx, x, y, 274 + }); 148 275 return; 149 276 } 150 277 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; 278 + // Try to grab a card 279 + const { tx, ty } = toTable(e.x, e.y); 280 + const idx = cardAtPos(tx, ty); 281 + if (idx >= 0) { 282 + const c = tableCards[idx]; 283 + // Can only grab if no one else is dragging it 284 + if (!c.dragger || c.dragger === myHandle) { 285 + dragging = bringToTop(idx); 286 + const nc = tableCards[dragging]; 287 + nc.dragger = myHandle; 288 + dragOffX = tx - nc.x; 289 + dragOffY = ty - nc.y; 290 + server?.send("table:grab", { 291 + handle: myHandle, cardIdx: nc.idx, 292 + }); 293 + } 294 + } 295 + } 296 + 297 + // Drag: move the card 298 + if (e.is("draw") && dragging !== null) { 299 + const { tx, ty } = toTable(e.x, e.y); 300 + const c = tableCards[dragging]; 301 + if (c) { 302 + c.x = tx - dragOffX; 303 + c.y = ty - dragOffY; 304 + } 305 + } 306 + 307 + // Lift: drop the card 308 + if (e.is("lift")) { 309 + if (dragging !== null) { 310 + const c = tableCards[dragging]; 311 + if (c) { 312 + c.dragger = null; 313 + server?.send("table:drop", { 314 + handle: myHandle, cardIdx: c.idx, x: c.x, y: c.y, 315 + }); 316 + } 317 + dragging = null; 318 + } 319 + } 320 + 321 + // Double-tap to flip (keyboard shortcut: f) 322 + if (e.is("keyboard:down:f")) { 323 + if (hoverIdx >= 0 && hoverIdx < tableCards.length) { 324 + const c = tableCards[hoverIdx]; 325 + c.faceUp = !c.faceUp; 326 + server?.send("table:flip", { handle: myHandle, cardIdx: c.idx }); 156 327 } 157 328 } 158 329 } 159 330 160 - function paint({ wipe, ink, box, write, screen }) { 331 + function paint({ wipe, ink, box, write, line, screen }) { 161 332 sw = screen.width; 162 333 sh = screen.height; 163 - wipe(245, 240, 230); 334 + wipe(60, 85, 55); // Green felt base 164 335 165 - // Felt area 166 - const pad = 10; 167 - ink(236, 230, 218).box(pad, pad, sw - pad * 2, sh - pad * 2); 336 + // -- Table surface -- 337 + const tl = toScreen(0, 0); 338 + const tw = TABLE_W; 339 + const th = TABLE_H; 168 340 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); 341 + // Table felt 342 + ink(70, 100, 65).box(tl.sx, tl.sy, tw, th); 343 + // Table border 344 + ink(90, 65, 40).box(tl.sx, tl.sy, tw, th, "outline"); 345 + ink(80, 58, 35).box(tl.sx - 1, tl.sy - 1, tw + 2, th + 2, "outline"); 186 346 347 + // -- Deck (table space, rendered in screen space) -- 348 + const ds = toScreen(DECK_X, DECK_Y); 187 349 if (deck.length > 0) { 188 - // Stack shadow 189 350 const n = Math.min(deck.length, 3); 190 351 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"); 352 + ink(205, 200, 190).box(ds.sx + i, ds.sy - i, CARD_W, CARD_H); 353 + ink(180, 175, 165).box(ds.sx + i, ds.sy - i, CARD_W, CARD_H, "outline"); 193 354 } 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 - 355 + ink(130, 100, 150).box(ds.sx + 2, ds.sy + 2, CARD_W - 4, CARD_H - 4); 356 + ink(150, 120, 170).box(ds.sx + 4, ds.sy + 4, CARD_W - 8, CARD_H - 8, "outline"); 198 357 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, 358 + ink(200, 195, 180).write(ct, { 359 + x: ds.sx + Math.floor(CARD_W / 2 - ct.length * 3), 360 + y: ds.sy + CARD_H + 2, 202 361 }); 203 362 } 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 - }); 363 + ink(80, 110, 75).box(ds.sx, ds.sy, CARD_W, CARD_H, "outline"); 209 364 } 210 365 211 - // -- Buttons -- 212 - paintBtn(ink, box, write, shuffleBtn, "shuf", true); 213 - paintBtn(ink, box, write, drawBtn, "draw", deck.length > 0); 366 + // -- Cards on table -- 367 + for (let i = 0; i < tableCards.length; i++) { 368 + const c = tableCards[i]; 369 + const s = toScreen(c.x, c.y); 370 + const isHover = i === hoverIdx && dragging === null; 371 + const isDragging = i === dragging; 372 + const isRemoteDrag = c.dragger && c.dragger !== myHandle; 214 373 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); 374 + // Shadow when dragging 375 + if (isDragging || isRemoteDrag) { 376 + ink(0, 0, 0, 30).box(s.sx + 2, s.sy + 2, CARD_W, CARD_H); 377 + } 220 378 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; 379 + if (c.faceUp) { 380 + const card = ALL[c.idx]; 381 + const col = card.suit === "hearts" || card.suit === "diamonds" ? RED : BLK; 226 382 227 383 // 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"); 384 + if (isHover) { 385 + ink(255, 255, 252).box(s.sx, s.sy, CARD_W, CARD_H); 386 + } else { 387 + ink(255, 253, 249).box(s.sx, s.sy, CARD_W, CARD_H); 388 + } 389 + 390 + // Border — highlight if hovered or being dragged 391 + if (isDragging) { 392 + ink(120, 160, 200).box(s.sx, s.sy, CARD_W, CARD_H, "outline"); 393 + } else if (isRemoteDrag) { 394 + ink(200, 140, 100).box(s.sx, s.sy, CARD_W, CARD_H, "outline"); 395 + } else if (isHover) { 396 + ink(140, 135, 130).box(s.sx, s.sy, CARD_W, CARD_H, "outline"); 397 + } else { 398 + ink(185, 180, 175).box(s.sx, s.sy, CARD_W, CARD_H, "outline"); 399 + } 230 400 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), 401 + // Rank + suit 402 + ink(...col).write(c.faceUp ? card.rank : "", { x: s.sx + 2, y: s.sy + 2 }); 403 + ink(...col).write(SYM[card.suit], { 404 + x: s.sx + Math.floor(CARD_W / 2 - 3), 405 + y: s.sy + Math.floor(CARD_H / 2 - 3), 237 406 }); 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, 407 + ink(...col).write(card.rank, { 408 + x: s.sx + CARD_W - card.rank.length * 6 - 2, 409 + y: s.sy + CARD_H - 9, 242 410 }); 411 + } else { 412 + // Face down 413 + ink(130, 100, 150).box(s.sx, s.sy, CARD_W, CARD_H); 414 + ink(150, 120, 170).box(s.sx + 2, s.sy + 2, CARD_W - 4, CARD_H - 4, "outline"); 415 + if (isHover) { 416 + ink(170, 145, 190).box(s.sx, s.sy, CARD_W, CARD_H, "outline"); 417 + } 243 418 } 419 + 420 + // Handle label for whoever is dragging this card 421 + if (c.dragger) { 422 + ink(255, 255, 255, 200).write(c.dragger, { 423 + x: s.sx, 424 + y: s.sy - 8, 425 + }); 426 + } 427 + } 428 + 429 + // -- HUD (screen space, on top) -- 430 + 431 + // Players list (top right) 432 + const pList = [myHandle]; 433 + for (const p of Object.values(players)) pList.push(p.handle); 434 + let py = 4; 435 + for (let i = 0; i < pList.length; i++) { 436 + const h = pList[i]; 437 + const label = i === 0 ? h + " (you)" : h; 438 + ink(220, 230, 215).write(label, { x: sw - label.length * 6 - 4, y: py }); 439 + py += 10; 440 + } 441 + 442 + // Buttons (bottom left) 443 + paintBtn(ink, box, write, shuffleBtn, "shuf", true); 444 + paintBtn(ink, box, write, drawBtn, "draw", deck.length > 0); 445 + 446 + // Deck count near draw button 447 + if (deck.length > 0) { 448 + const dl = "" + deck.length; 449 + ink(200, 210, 195).write(dl, { 450 + x: drawBtn.x + BTN_W + 4, 451 + y: drawBtn.y + 3, 452 + }); 244 453 } 245 454 246 455 // 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 - }); 456 + const hint = dragging !== null 457 + ? "dragging..." 458 + : hoverIdx >= 0 459 + ? "drag to move / f to flip" 460 + : "tap deck or draw"; 461 + ink(180, 195, 175).write(hint, { x: 4, y: 4 }); 252 462 } 253 463 254 464 function paintBtn(ink, box, write, btn, label, active) { 255 465 if (!btn) return; 256 466 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 }); 467 + ink(55, 75, 50).box(btn.x, btn.y, btn.w, btn.h); 468 + ink(80, 110, 72).box(btn.x, btn.y, btn.w, btn.h, "outline"); 469 + ink(200, 210, 195).write(label, { x: btn.x + 6, y: btn.y + 3 }); 260 470 } 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 }); 471 + ink(50, 65, 45).box(btn.x, btn.y, btn.w, btn.h); 472 + ink(65, 80, 58).box(btn.x, btn.y, btn.w, btn.h, "outline"); 473 + ink(100, 115, 95).write(label, { x: btn.x + 6, y: btn.y + 3 }); 264 474 } 265 475 } 266 476 267 477 function meta() { 268 478 return { 269 479 title: "Table", 270 - desc: "Multiplayer card table. Shuffle, draw, and see who's at the table.", 480 + desc: "Multiplayer card table. Drag cards on a shared surface.", 271 481 }; 272 482 } 273 483 274 - export { boot, act, paint, meta }; 484 + export { boot, sim, act, paint, meta }; 275 485 export const desc = "Multiplayer card table.";