Files
troxler-skat/card-game/generate_scorepad.js
2026-05-31 14:39:45 -05:00

248 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.');