card game
This commit is contained in:
247
card-game/generate_scorepad.js
Normal file
247
card-game/generate_scorepad.js
Normal file
@@ -0,0 +1,247 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PDFDocument = require('pdfkit');
|
||||
|
||||
/* ============================================================
|
||||
TROX – Scorepad Generator (PDF, parametrisierbar)
|
||||
------------------------------------------------------------
|
||||
Ausgabe als PDF. Frei einstellbar: Format, Ausrichtung,
|
||||
Spalten/Zeilen an Cards, Spielerzahl, Seitenrand.
|
||||
|
||||
AUFRUF:
|
||||
node generate_scorepad.js [format] [orientation] [cols] [rows] [players] [margin_in]
|
||||
|
||||
Beispiele:
|
||||
node generate_scorepad.js -> both, portrait, 2x3, 6 Spieler
|
||||
node generate_scorepad.js letter portrait 1 2 6 -> 2 große Cards/Seite
|
||||
node generate_scorepad.js a4 landscape 3 2 4 -> A4 quer, 6 Cards, 4 Spieler
|
||||
node generate_scorepad.js letter landscape 2 2 6 0.4
|
||||
|
||||
Parameter:
|
||||
format letter | a4 | both (Standard: both)
|
||||
orientation portrait | landscape (Standard: portrait)
|
||||
cols Spalten an Cards (Standard: 2)
|
||||
rows Zeilen an Cards (Standard: 3)
|
||||
players Spieler pro Card (Standard: 6)
|
||||
margin_in Seitenrand in Zoll (Standard: 0.4)
|
||||
|
||||
Eigenschaften:
|
||||
- "Position"-Zeile WEISS (zum Eintragen der Endplatzierung, z.B. 1. / 3.)
|
||||
- "Name" links oben in der Kopfzelle, dezent/schwach gedruckt
|
||||
- TROX-Titelbalken weiß mit goldenen Verzierungen (nicht schwarz)
|
||||
- Bid-Kästchen kräftig gedruckt
|
||||
============================================================ */
|
||||
|
||||
// ---------- CONFIG (Defaults; per CLI überschreibbar) ----------
|
||||
const CONFIG = {
|
||||
cols: 2,
|
||||
rows: 3,
|
||||
players: 6,
|
||||
marginIn: 0.4,
|
||||
gapIn: 0.14,
|
||||
orientation: 'portrait',
|
||||
};
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const MODE = (argv[0] || 'both').toLowerCase();
|
||||
if (argv[1] && /^(portrait|landscape)$/.test(argv[1])) CONFIG.orientation = argv[1];
|
||||
if (argv[2] !== undefined && !isNaN(+argv[2])) CONFIG.cols = Math.max(1, parseInt(argv[2]));
|
||||
if (argv[3] !== undefined && !isNaN(+argv[3])) CONFIG.rows = Math.max(1, parseInt(argv[3]));
|
||||
if (argv[4] !== undefined && !isNaN(+argv[4])) CONFIG.players = Math.max(2, parseInt(argv[4]));
|
||||
if (argv[5] !== undefined && !isNaN(+argv[5])) CONFIG.marginIn = Math.max(0, parseFloat(argv[5]));
|
||||
|
||||
const PT = 72; // Punkte pro Zoll (PDF-Einheit)
|
||||
|
||||
// Palette
|
||||
const C = {
|
||||
dark: '#111827',
|
||||
gold: '#AA7C11',
|
||||
goldLt: '#D4AF37',
|
||||
goldPale:'#F3E5AB',
|
||||
ink: '#1F2937',
|
||||
line: '#D1D5DB',
|
||||
zebra: '#FAFAFA',
|
||||
nameTx: '#C4C9D2', // schwach gedruckter Name
|
||||
bidStroke: '#6B7280', // kräftiger als vorher (#cbd5e1)
|
||||
grey: '#9CA3AF',
|
||||
};
|
||||
|
||||
const ROUNDS = [
|
||||
'R1 (1 Card)', 'R2 (2 Cards)', 'R3 (3 Cards)', 'R4 (4 Cards)', 'R5 (5 Cards)',
|
||||
'R6 (5 Cards)', 'R7 (4 Cards)', 'R8 (3 Cards)', 'R9 (2 Cards)', 'R10 (1 Card)'
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// Eine Scorecard zeichnen (Koordinaten in PDF-Punkten)
|
||||
// ============================================================
|
||||
function drawCard(doc, x, y, w, h, players) {
|
||||
const sc = Math.min(Math.max(w / 270, 0.7), 2.2); // Skalenfaktor (270pt ~ Referenz)
|
||||
|
||||
const titleH = 24 * sc;
|
||||
const headerH = 22 * sc;
|
||||
const bodyTop = titleH + headerH;
|
||||
const nRows = ROUNDS.length + 1; // + Position
|
||||
const rowH = (h - bodyTop) / nRows;
|
||||
|
||||
const labelColW = w * 0.26;
|
||||
const playerColW = (w - labelColW) / players;
|
||||
|
||||
const fTitle = 12 * sc, fHeader = 7.5 * sc, fRound = 7 * sc,
|
||||
fName = 6.8 * sc, fPos = 8 * sc;
|
||||
const bidW = 12 * sc, bidH = 9 * sc;
|
||||
const radius = 6 * sc;
|
||||
|
||||
// --- Outer frame (weißer Hintergrund) ---
|
||||
doc.save();
|
||||
doc.roundedRect(x, y, w, h, radius).fillAndStroke('#ffffff', C.ink);
|
||||
doc.lineWidth(1.2).strokeColor(C.ink).roundedRect(x, y, w, h, radius).stroke();
|
||||
doc.restore();
|
||||
|
||||
// --- Titelbalken: WEISS mit goldenen Verzierungen ---
|
||||
doc.save();
|
||||
// dünne goldene Linien ober-/unterhalb + Mittelornamente
|
||||
const tcy = y + titleH / 2;
|
||||
doc.font('Helvetica-Bold').fontSize(fTitle).fillColor(C.gold)
|
||||
.text('T R O X', x, tcy - fTitle * 0.62, { width: w, align: 'center', characterSpacing: 2 * sc });
|
||||
// Verzierungslinien links/rechts vom Titel
|
||||
const titleTextW = doc.widthOfString('T R O X', { characterSpacing: 2 * sc });
|
||||
const cxp = x + w / 2;
|
||||
const ornGap = titleTextW / 2 + 12 * sc;
|
||||
const ornLen = Math.max(8 * sc, (w / 2) - ornGap - 10 * sc);
|
||||
doc.lineWidth(1 * sc).strokeColor(C.goldLt).opacity(0.9);
|
||||
// links
|
||||
doc.moveTo(x + 10 * sc, tcy).lineTo(x + 10 * sc + ornLen, tcy).stroke();
|
||||
// rechts
|
||||
doc.moveTo(x + w - 10 * sc - ornLen, tcy).lineTo(x + w - 10 * sc, tcy).stroke();
|
||||
// kleine Rauten an den Linienenden
|
||||
const diamond = (dx) => {
|
||||
const r = 2.4 * sc;
|
||||
doc.opacity(1).fillColor(C.goldLt)
|
||||
.moveTo(dx, tcy - r).lineTo(dx + r, tcy).lineTo(dx, tcy + r).lineTo(dx - r, tcy).fill();
|
||||
};
|
||||
diamond(x + 10 * sc); diamond(x + 10 * sc + ornLen);
|
||||
diamond(x + w - 10 * sc); diamond(x + w - 10 * sc - ornLen);
|
||||
doc.opacity(1);
|
||||
// feine goldene Trennlinie unter dem Titel
|
||||
doc.lineWidth(0.8 * sc).strokeColor(C.goldLt)
|
||||
.moveTo(x, y + titleH).lineTo(x + w, y + titleH).stroke();
|
||||
doc.restore();
|
||||
|
||||
// --- Kopfzeile ---
|
||||
const hy = y + titleH;
|
||||
doc.save();
|
||||
doc.font('Helvetica-Bold').fontSize(fHeader).fillColor(C.ink)
|
||||
.text('Round (Cards)', x + 6 * sc, hy + headerH / 2 - fHeader * 0.55, { width: labelColW - 8 * sc });
|
||||
// Label-Spalten-Trennlinie über volle Höhe
|
||||
doc.lineWidth(1).strokeColor(C.ink)
|
||||
.moveTo(x + labelColW, hy).lineTo(x + labelColW, y + h).stroke();
|
||||
|
||||
for (let p = 0; p < players; p++) {
|
||||
const px = x + labelColW + p * playerColW;
|
||||
// Kopfzelle: heller Hintergrund
|
||||
doc.rect(px, hy, playerColW, headerH).fillColor('#F3F4F6').fill();
|
||||
doc.lineWidth(0.5).strokeColor(C.ink).rect(px, hy, playerColW, headerH).stroke();
|
||||
// "Name" links oben, schwach
|
||||
doc.font('Helvetica-Oblique').fontSize(fName).fillColor(C.nameTx)
|
||||
.text('Name', px + 4 * sc, hy + 3 * sc, { width: playerColW - 6 * sc });
|
||||
if (p < players - 1) {
|
||||
doc.lineWidth(0.5).strokeColor(C.ink)
|
||||
.moveTo(px + playerColW, hy).lineTo(px + playerColW, y + h).stroke();
|
||||
}
|
||||
}
|
||||
doc.restore();
|
||||
|
||||
// --- Runden-Zeilen ---
|
||||
for (let r = 0; r < ROUNDS.length; r++) {
|
||||
const ry = y + bodyTop + r * rowH;
|
||||
doc.save();
|
||||
if (r % 2 === 1) {
|
||||
doc.rect(x + labelColW, ry, w - labelColW, rowH).fillColor(C.zebra).fill();
|
||||
}
|
||||
doc.lineWidth(0.5).strokeColor(C.line).moveTo(x, ry).lineTo(x + w, ry).stroke();
|
||||
doc.font('Helvetica').fontSize(fRound).fillColor(C.ink)
|
||||
.text(ROUNDS[r], x + 6 * sc, ry + rowH / 2 - fRound * 0.55, { width: labelColW - 8 * sc });
|
||||
// Bid-Kästchen je Spieler – KRÄFTIGER Strich
|
||||
for (let p = 0; p < players; p++) {
|
||||
const px = x + labelColW + p * playerColW;
|
||||
doc.lineWidth(1.1 * sc).strokeColor(C.bidStroke)
|
||||
.roundedRect(px + 3 * sc, ry + 2.5 * sc, bidW, bidH, 1.5 * sc).stroke();
|
||||
}
|
||||
doc.restore();
|
||||
}
|
||||
|
||||
// --- Position-Zeile: WEISS (zum Eintragen der Platzierung) ---
|
||||
const py = y + bodyTop + ROUNDS.length * rowH;
|
||||
doc.save();
|
||||
// weißer Hintergrund (kein dunkler Balken mehr)
|
||||
doc.rect(x, py, w, rowH).fillColor('#ffffff').fill();
|
||||
// kräftige Trennlinie oben drüber
|
||||
doc.lineWidth(1).strokeColor(C.ink).moveTo(x, py).lineTo(x + w, py).stroke();
|
||||
doc.font('Helvetica-Bold').fontSize(fPos).fillColor(C.gold)
|
||||
.text('Position', x + 6 * sc, py + rowH / 2 - fPos * 0.55, { width: labelColW - 8 * sc });
|
||||
// vertikale Spaltenlinien in der Position-Zeile
|
||||
for (let p = 1; p < players; p++) {
|
||||
const px = x + labelColW + p * playerColW;
|
||||
doc.lineWidth(0.5).strokeColor(C.line).moveTo(px, py).lineTo(px, py + rowH).stroke();
|
||||
}
|
||||
doc.restore();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Eine Seite zeichnen
|
||||
// ============================================================
|
||||
function drawPage(doc, pageW, pageH, label) {
|
||||
const margin = CONFIG.marginIn * PT;
|
||||
const gap = CONFIG.gapIn * PT;
|
||||
const headerSpace = 22;
|
||||
|
||||
const { cols, rows: rowsN, players } = CONFIG;
|
||||
const cardW = (pageW - 2 * margin - (cols - 1) * gap) / cols;
|
||||
const cardH = (pageH - 2 * margin - headerSpace - (rowsN - 1) * gap) / rowsN;
|
||||
|
||||
// Seitenkopf
|
||||
doc.font('Helvetica-Bold').fontSize(11).fillColor(C.dark)
|
||||
.text('TROX SCOREPAD', margin, margin, { characterSpacing: 1.5, continued: false });
|
||||
const gpp = cols * rowsN;
|
||||
doc.font('Helvetica').fontSize(7).fillColor(C.grey)
|
||||
.text(`${gpp} game(s) / ${label} ${CONFIG.orientation} · ${players} players · box = Bid · cell = running total · exact bid = +10`,
|
||||
margin, margin + 1, { width: pageW - 2 * margin, align: 'right' });
|
||||
|
||||
for (let r = 0; r < rowsN; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const cx = margin + c * (cardW + gap);
|
||||
const cy = margin + headerSpace + r * (cardH + gap);
|
||||
drawCard(doc, cx, cy, cardW, cardH, players);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
const FORMATS = {
|
||||
letter: { size: 'LETTER', label: 'Letter' },
|
||||
a4: { size: 'A4', label: 'A4' },
|
||||
};
|
||||
|
||||
const outputDir = path.join(__dirname, 'scorepad_export');
|
||||
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
function emit(key) {
|
||||
const fmt = FORMATS[key];
|
||||
const doc = new PDFDocument({
|
||||
size: fmt.size,
|
||||
layout: CONFIG.orientation,
|
||||
margin: 0,
|
||||
info: { Title: `TROX Scorepad (${fmt.label})`, Author: 'Bay Area Affiliates' },
|
||||
});
|
||||
const file = `trox_scorepad_${key}_${CONFIG.orientation}_${CONFIG.cols}x${CONFIG.rows}_${CONFIG.players}p.pdf`;
|
||||
const outPath = path.join(outputDir, file);
|
||||
doc.pipe(fs.createWriteStream(outPath));
|
||||
drawPage(doc, doc.page.width, doc.page.height, fmt.label);
|
||||
doc.end();
|
||||
console.log(` ${fmt.label} ${CONFIG.orientation} -> scorepad_export/${file}`);
|
||||
}
|
||||
|
||||
console.log(`TROX – Scorepad (PDF): ${CONFIG.cols}x${CONFIG.rows} Cards, ${CONFIG.players} Spieler, ${CONFIG.orientation}, Rand ${CONFIG.marginIn}"`);
|
||||
if (MODE === 'letter' || MODE === 'both') emit('letter');
|
||||
if (MODE === 'a4' || MODE === 'both') emit('a4');
|
||||
console.log('Fertig.');
|
||||
Reference in New Issue
Block a user