Initial commit: Shelem card game

Full-stack multiplayer Shelem (Iranian trick-taking card game) with
Socket.IO, JWT auth, bot players, joker mode, and mobile-friendly UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-05-08 16:17:37 +00:00
commit 8e8478e45b
12 changed files with 3869 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
'use strict';
// Generates PNG icons with a Joker card motif on dark green background.
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const outDir = path.join(__dirname, 'public', 'icons');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
function writePNG(filePath, size) {
const channels = 4;
const row = size * channels;
const raw = Buffer.alloc(size * row);
// Background: dark forest green #133025
const bgR = 0x13, bgG = 0x30, bgB = 0x25;
// Gold color for Joker diamond: #f5c518
const gR = 0xf5, gG = 0xc5, gB = 0x18;
const cx = size / 2, cy = size / 2;
const dh = size * 0.35; // half-height of diamond
const dw = size * 0.28; // half-width
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const px = (y * size + x) * channels;
const nx = x + 0.5, ny = y + 0.5;
// Rounded corners
const cr = size * 0.2;
const inCorner =
(nx < cr && ny < cr && Math.hypot(nx - cr, ny - cr) > cr) ||
(nx > size - cr && ny < cr && Math.hypot(nx - (size - cr), ny - cr) > cr) ||
(nx < cr && ny > size - cr && Math.hypot(nx - cr, ny - (size - cr)) > cr) ||
(nx > size - cr && ny > size - cr && Math.hypot(nx - (size - cr), ny - (size - cr)) > cr);
if (inCorner) { raw[px + 3] = 0; continue; }
// Draw a diamond / rhombus shape (Joker card symbol)
const dx = Math.abs(nx - cx) / dw;
const dy = Math.abs(ny - cy) / dh;
const inDiamond = dx + dy <= 1;
if (inDiamond) {
raw[px] = gR;
raw[px + 1] = gG;
raw[px + 2] = gB;
raw[px + 3] = 255;
} else {
raw[px] = bgR;
raw[px + 1] = bgG;
raw[px + 2] = bgB;
raw[px + 3] = 255;
}
}
}
const chunks = [];
chunks.push(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
function crc32(buf) {
let c = 0xffffffff;
const table = crc32.table || (crc32.table = (() => {
const t = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let v = i;
for (let k = 0; k < 8; k++) v = v & 1 ? 0xedb88320 ^ (v >>> 1) : v >>> 1;
t[i] = v;
}
return t;
})());
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const tp = Buffer.from(type);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(Buffer.concat([tp, data])));
return Buffer.concat([len, tp, data, crc]);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(size, 0); ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8; ihdr[9] = 6;
chunks.push(chunk('IHDR', ihdr));
const filtered = Buffer.alloc(size * (row + 1));
for (let y = 0; y < size; y++) {
filtered[y * (row + 1)] = 0;
raw.copy(filtered, y * (row + 1) + 1, y * row, (y + 1) * row);
}
chunks.push(chunk('IDAT', zlib.deflateSync(filtered)));
chunks.push(chunk('IEND', Buffer.alloc(0)));
fs.writeFileSync(filePath, Buffer.concat(chunks));
console.log(`${path.basename(filePath)} written (${size}×${size})`);
}
writePNG(path.join(outDir, 'icon-192.png'), 192);
writePNG(path.join(outDir, 'icon-512.png'), 512);
// SVG icon (scalable, used as favicon)
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#133025"/>
<polygon points="50,10 80,50 50,90 20,50" fill="#f5c518"/>
<text x="50" y="56" font-size="22" text-anchor="middle" fill="#133025" font-family="serif" font-weight="bold">J</text>
</svg>`;
fs.writeFileSync(path.join(outDir, 'icon.svg'), svg);
console.log('icon.svg written');
console.log('Icons generated.');