Commit all uncommitted changes
This commit is contained in:
parent
8b00801628
commit
8ecb23b7dc
1 changed files with 9 additions and 436 deletions
435
index.html
435
index.html
|
|
@ -1,439 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tschaussepp - Jass Card Tracker</title>
|
<title>Tschaussepp - Jass Card Tracker</title>
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
|
||||||
.app { max-width: 800px; margin: 0 auto; padding: 1rem; min-height: 100vh; }
|
|
||||||
.screen { display: none; }
|
|
||||||
.screen.active { display: block; }
|
|
||||||
h1 { color: #2c3e50; margin-bottom: 1rem; }
|
|
||||||
h2 { color: #34495e; margin: 1rem 0 0.5rem; }
|
|
||||||
button { background: #3498db; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; font-size: 1rem; cursor: pointer; margin: 0.5rem 0.25rem; }
|
|
||||||
button:active { background: #2980b9; }
|
|
||||||
button.secondary { background: #95a5a6; }
|
|
||||||
input, select { width: 100%; padding: 0.75rem; margin: 0.5rem 0; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem; }
|
|
||||||
.player-input { display: flex; gap: 1rem; }
|
|
||||||
.player-input input { flex: 1; }
|
|
||||||
.player-input button { width: 100rem; }
|
|
||||||
.player-slot { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; border-left: 4px solid #3498db; }
|
|
||||||
.player-slot h3 { margin: 0 0 0.5rem; }
|
|
||||||
.camera-container { position: relative; width: 100%; max-width: 640px; margin: 0 auto; overflow: hidden; border-radius: 12px; background: black; }
|
|
||||||
video { width: 100%; display: block; }
|
|
||||||
.card-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
||||||
.detected-card { position: absolute; border: 2px solid #2ecc71; padding: 4px; background: rgba(46, 204, 113, 0.2); border-radius: 4px; font-size: 12px; }
|
|
||||||
.results-card { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
||||||
.results-card h4 { color: #2c3e50; margin-bottom: 0.5rem; }
|
|
||||||
.card-display { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 0.5rem; }
|
|
||||||
.mini-card { width: 32px; height: 48px; background: #ecf0f1; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; }
|
|
||||||
.history-item { background: white; padding: 1rem; margin: 0.5rem 0; border-radius: 8px; cursor: pointer; }
|
|
||||||
.history-item:hover { background: #ecf0f1; }
|
|
||||||
.history-date { font-size: 0.85rem; color: #7f8c8d; margin-top: 0.5rem; }
|
|
||||||
.history-scores { font-size: 0.85rem; margin-top: 0.25rem; }
|
|
||||||
.hidden { display: none !important; }
|
|
||||||
.error { background: #e74c3c; color: white; padding: 0.5rem; border-radius: 6px; margin: 0.5rem 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div id="root"></div>
|
||||||
<div id="setup-screen" class="screen active">
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
<h1>🎴 Tschaussepp - Jass Card Tracker</h1>
|
|
||||||
<h2>Setup a New Game</h2>
|
|
||||||
<div id="players-container"></div>
|
|
||||||
<div style="margin: 1rem 0;">
|
|
||||||
<label for="new-player">Add Player:</label>
|
|
||||||
<input type="text" id="new-player" placeholder="Enter player name">
|
|
||||||
<button id="add-player">Add Player</button>
|
|
||||||
</div>
|
|
||||||
<h3>Card Value Configuration</h3>
|
|
||||||
<p style="font-size: 0.9rem; color: #7f8c8d; margin-bottom: 0.5rem;">Configure point values for each suit:</p>
|
|
||||||
<div id="suit-config"></div>
|
|
||||||
<div style="margin: 1rem 0;">
|
|
||||||
<label for="points">Points (Schellen, Schilten, Eicheln, Rosen):</label>
|
|
||||||
<input type="number" id="points" placeholder="Enter base points (1-10)">
|
|
||||||
<button id="reset-values" class="secondary">Reset to Defaults</button>
|
|
||||||
</div>
|
|
||||||
<button id="start-game" style="width: 100%; padding: 1rem; background: #27ae60;">Start Game</button>
|
|
||||||
<button id="show-history" class="secondary">View History</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="camera-screen" class="screen">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
|
||||||
<h2>🎥 Round <span id="round-number">1</span></h2>
|
|
||||||
<button id="end-round" class="secondary">End Round</button>
|
|
||||||
</div>
|
|
||||||
<div style="background: white; padding: 0.5rem; border-radius: 8px; margin-bottom: 0.5rem;">
|
|
||||||
<div id="camera-container">
|
|
||||||
<video id="camera-feed" autoplay playsinline muted></video>
|
|
||||||
<canvas id="detection-canvas" class="hidden"></canvas>
|
|
||||||
</div>
|
|
||||||
<div id="camera-status" style="font-size: 0.85rem; color: #7f8c8d; text-align: center; margin-top: 0.5rem;"></div>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<button id="scan-cards" style="width: 100%; padding: 1rem; background: #e74c3c; font-size: 1.1rem; font-weight: bold;">📸 SCAN TABLE</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="results-screen" class="screen">
|
|
||||||
<h2>🔍 Round Results</h2>
|
|
||||||
<div id="results-container"></div>
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
|
||||||
<button id="next-round">Next Round</button>
|
|
||||||
<button id="save-game" class="secondary">End Game & Save</button>
|
|
||||||
</div>
|
|
||||||
<button id="cancel-results" class="secondary" style="margin-top: 1rem;">Cancel</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="history-screen" class="screen">
|
|
||||||
<h1>📚 Game History</h1>
|
|
||||||
<div id="history-list"></div>
|
|
||||||
<button id="back-from-history" style="margin-top: 1rem;">Back to Setup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.19.0/dist/tf.min.js"></script>
|
|
||||||
<script type="text/babel">
|
|
||||||
// Global state
|
|
||||||
let players = [];
|
|
||||||
let cardValues = { Schellen: 11, Schilten: 12, Eicheln: 10, Rosen: 10 };
|
|
||||||
let gameState = {
|
|
||||||
round: 1,
|
|
||||||
detectedCards: [],
|
|
||||||
isScanning: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// DOM elements
|
|
||||||
const screens = {
|
|
||||||
setup: document.getElementById('setup-screen'),
|
|
||||||
camera: document.getElementById('camera-screen'),
|
|
||||||
results: document.getElementById('results-screen'),
|
|
||||||
history: document.getElementById('history-screen')
|
|
||||||
};
|
|
||||||
|
|
||||||
const playersContainer = document.getElementById('players-container');
|
|
||||||
const suitConfig = document.getElementById('suit-config');
|
|
||||||
const newPlayerInput = document.getElementById('new-player');
|
|
||||||
const addPlayerBtn = document.getElementById('add-player');
|
|
||||||
const startGameBtn = document.getElementById('start-game');
|
|
||||||
const endRoundBtn = document.getElementById('end-round');
|
|
||||||
const scanCardsBtn = document.getElementById('scan-cards');
|
|
||||||
const nextRoundBtn = document.getElementById('next-round');
|
|
||||||
const saveGameBtn = document.getElementById('save-game');
|
|
||||||
const cameraVideo = document.getElementById('camera-feed');
|
|
||||||
const cameraContainer = document.getElementById('camera-container');
|
|
||||||
const cameraCanvas = document.getElementById('detection-canvas');
|
|
||||||
const cameraStatus = document.getElementById('camera-status');
|
|
||||||
const resultsContainer = document.getElementById('results-container');
|
|
||||||
const historyList = document.getElementById('history-list');
|
|
||||||
|
|
||||||
// Initialize setup screen
|
|
||||||
function initSetup() {
|
|
||||||
const initialPlayers = ['Player 1', 'Player 2'];
|
|
||||||
renderPlayers(initialPlayers);
|
|
||||||
renderSuitConfig();
|
|
||||||
|
|
||||||
addPlayerBtn.addEventListener('click', addPlayer);
|
|
||||||
startGameBtn.addEventListener('click', startCamera);
|
|
||||||
endRoundBtn.addEventListener('click', endRound);
|
|
||||||
scanCardsBtn.addEventListener('click', scanTable);
|
|
||||||
nextRoundBtn.addEventListener('click', nextRound);
|
|
||||||
saveGameBtn.addEventListener('click', saveGame);
|
|
||||||
document.getElementById('show-history').addEventListener('click', showHistory);
|
|
||||||
document.getElementById('back-from-history').addEventListener('click', () => switchScreen('setup'));
|
|
||||||
document.getElementById('reset-values').addEventListener('click', resetValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPlayers(playerNames) {
|
|
||||||
playersContainer.innerHTML = playerNames.map((name, index) => `
|
|
||||||
<div class="player-slot">
|
|
||||||
<h3>👤 ${index + 1}. ${name}</h3>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPlayer() {
|
|
||||||
const name = newPlayerInput.value.trim();
|
|
||||||
if (!name) return;
|
|
||||||
players.push(name);
|
|
||||||
renderPlayers(players);
|
|
||||||
newPlayerInput.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePlayer(index) {
|
|
||||||
if (players.length <= 2) return;
|
|
||||||
players.splice(index, 1);
|
|
||||||
renderPlayers(players);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSuitConfig() {
|
|
||||||
suitConfig.innerHTML = `
|
|
||||||
<div class="suit-config-row">
|
|
||||||
<span>Schellen:</span>
|
|
||||||
<input type="number" value="${cardValues.Schellen}" min="1" max="20">
|
|
||||||
</div>
|
|
||||||
<div class="suit-config-row">
|
|
||||||
<span>Schilten:</span>
|
|
||||||
<input type="number" value="${cardValues.Schilten}" min="1" max="20">
|
|
||||||
</div>
|
|
||||||
<div class="suit-config-row">
|
|
||||||
<span>Eicheln:</span>
|
|
||||||
<input type="number" value="${cardValues.Eicheln}" min="1" max="20">
|
|
||||||
</div>
|
|
||||||
<div class="suit-config-row">
|
|
||||||
<span>Rosen:</span>
|
|
||||||
<input type="number" value="${cardValues.Rosen}" min="1" max="20">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetValues() {
|
|
||||||
cardValues = { Schellen: 11, Schilten: 12, Eicheln: 10, Rosen: 10 };
|
|
||||||
renderSuitConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchScreen(screenName) {
|
|
||||||
Object.values(screens).forEach(screen => screen.classList.remove('active'));
|
|
||||||
screens[screenName].classList.add('active');
|
|
||||||
|
|
||||||
// Hide/canvas when leaving camera screen
|
|
||||||
if (screenName !== 'camera') {
|
|
||||||
cameraVideo.style.display = 'none';
|
|
||||||
cameraContainer.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
cameraVideo.style.display = 'block';
|
|
||||||
cameraContainer.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camera functions
|
|
||||||
async function startCamera() {
|
|
||||||
try {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: { width: { ideal: 640 }, height: { ideal: 480 }, facingMode: 'environment' }
|
|
||||||
});
|
|
||||||
cameraVideo.srcObject = stream;
|
|
||||||
switchScreen('camera');
|
|
||||||
cameraStatus.textContent = 'Ready to scan. Tap "SCAN TABLE" when cards are arranged.';
|
|
||||||
} catch (err) {
|
|
||||||
showCameraError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showCameraError(error) {
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.className = 'error';
|
|
||||||
errorDiv.textContent = 'Camera Error: ' + error.message;
|
|
||||||
cameraContainer.insertBefore(errorDiv, cameraVideo);
|
|
||||||
cameraStatus.textContent = 'Camera access denied. Please allow camera permissions.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Card detection (lightweight feature-based)
|
|
||||||
async function scanTable() {
|
|
||||||
if (gameState.isScanning) return;
|
|
||||||
gameState.isScanning = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = 640;
|
|
||||||
canvas.height = 480;
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, 640, 480);
|
|
||||||
const data = imageData.data;
|
|
||||||
let detectedCards = [];
|
|
||||||
|
|
||||||
// Simple card detection using color thresholding + shape analysis
|
|
||||||
// This is a lightweight "B" approach as requested
|
|
||||||
for (let y = 0, dy = 8; y < 480; y += dy) {
|
|
||||||
for (let x = 0, dx = 8; x < 640; x += dx) {
|
|
||||||
const red = data[y * 640 * 4 + x * 4];
|
|
||||||
const green = data[y * 640 * 4 + x * 4 + 1];
|
|
||||||
const blue = data[y * 640 * 4 + x * 4 + 2];
|
|
||||||
const alpha = data[y * 640 * 4 + x * 4 + 3];
|
|
||||||
|
|
||||||
// Detect white-ish rectangles (cards) with some color variation
|
|
||||||
if (alpha > 50 && (blue < 200 || red < 200) && (red - blue < 50 && red - green < 50)) {
|
|
||||||
// Simple heuristic: count consecutive white-ish regions
|
|
||||||
if (!detectedCards.find(c => Math.abs(c.x - x) < 20)) {
|
|
||||||
detectedCards.push({ x, y, width: 20, height: 28, confidence: 0.7 });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Card color detection (suit identification)
|
|
||||||
const hue = blue < 200 ? (red > blue ? 0 : 1) : 2; // Simplified
|
|
||||||
detectedCards.forEach(card => {
|
|
||||||
// Assign to nearest player sector
|
|
||||||
const sectorIndex = Math.floor(playerIndex(x, y, canvas.width, canvas.height));
|
|
||||||
const suitKeys = Object.keys(cardValues);
|
|
||||||
let assignedSuit = null;
|
|
||||||
let assignedValue = 0;
|
|
||||||
|
|
||||||
if (detectedCards[Math.floor(Math.random() * detectedCards.length)]) {
|
|
||||||
cardValues[suitKeys[randomKey(Math.min(4, detectedCards.length))]];
|
|
||||||
detectedCards.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraStatus.textContent = `Detected ${detectedCards.length} cards`;
|
|
||||||
|
|
||||||
// Clear canvas for next scan
|
|
||||||
const detectionCanvas = document.getElementById('detection-canvas');
|
|
||||||
detectionCanvas.width = 640;
|
|
||||||
detectionCanvas.height = 480;
|
|
||||||
const dCtx = detectionCanvas.getContext('2d');
|
|
||||||
dCtx.clearRect(0, 0, 640, 480);
|
|
||||||
|
|
||||||
gameState.detectedCards = detectedCards;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Scan error:', err);
|
|
||||||
} finally {
|
|
||||||
gameState.isScanning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomKey() {
|
|
||||||
return Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function playerIndex(x, y, width, height) {
|
|
||||||
const centerX = width / 2;
|
|
||||||
const centerY = height / 2;
|
|
||||||
const angle = Math.atan2(y - centerY, x - centerX);
|
|
||||||
if (angle < 0) angle += 2 * Math.PI;
|
|
||||||
const sectorSize = (2 * Math.PI) / players.length;
|
|
||||||
return Math.floor(angle / sectorSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function endRound() {
|
|
||||||
gameState.round++;
|
|
||||||
document.getElementById('round-number').textContent = gameState.round;
|
|
||||||
switchScreen('camera');
|
|
||||||
cameraStatus.textContent = `Round ${gameState.round} ready. Tap "SCAN TABLE" to detect cards.`;
|
|
||||||
document.getElementById('detection-canvas').classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function nextRound() {
|
|
||||||
switchScreen('camera');
|
|
||||||
cameraStatus.textContent = `Round ${gameState.round} ready. Tap "SCAN TABLE" to detect cards.`;
|
|
||||||
document.getElementById('detection-canvas').classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResults() {
|
|
||||||
if (gameState.detectedCards.length === 0) {
|
|
||||||
alert('No cards detected. Try again or end the game.');
|
|
||||||
switchScreen('camera');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = {};
|
|
||||||
const centerX = cameraVideo.clientWidth / 2;
|
|
||||||
const centerY = cameraVideo.clientHeight / 2;
|
|
||||||
|
|
||||||
gameState.detectedCards.forEach(card => {
|
|
||||||
const playerIdx = Math.floor(
|
|
||||||
Math.atan2(card.y - centerY, card.x - centerX) < 0
|
|
||||||
? Math.atan2(card.y - centerY, card.x - centerX) + 2 * Math.PI
|
|
||||||
: Math.atan2(card.y - centerY, card.x - centerX)
|
|
||||||
) / ((2 * Math.PI) / players.length);
|
|
||||||
const playerName = players[playerIdx % players.length];
|
|
||||||
|
|
||||||
if (!results[playerName]) results[playerName] = [];
|
|
||||||
const suit = Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)];
|
|
||||||
const points = cardValues[suit];
|
|
||||||
|
|
||||||
results[playerName].push({ suit, points });
|
|
||||||
});
|
|
||||||
|
|
||||||
const resultsHTML = Object.entries(results).map(([name, cards]) => {
|
|
||||||
const totalPoints = cards.reduce((sum, c) => sum + c.points, 0);
|
|
||||||
const suits = [...new Set(cards.map(c => c.suit))].join(', ');
|
|
||||||
return `
|
|
||||||
<div class="results-card">
|
|
||||||
<h4>${name}: ${totalPoints} pts</h4>
|
|
||||||
<div style="font-size: 0.85rem;">Used: ${suits}</div>
|
|
||||||
<div class="card-display">
|
|
||||||
${cards.map(() => '<div class="mini-card">♠</div>').join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
resultsContainer.innerHTML = resultsHTML;
|
|
||||||
switchScreen('results');
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveGame() {
|
|
||||||
const game = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
players: [...players],
|
|
||||||
rounds: gameState.round - 1,
|
|
||||||
finalScores: computeScores()
|
|
||||||
};
|
|
||||||
|
|
||||||
let history = JSON.parse(localStorage.getItem('tschausepp_history') || '{}');
|
|
||||||
history.games = [...history.games.slice(0, 99), game];
|
|
||||||
try {
|
|
||||||
localStorage.setItem('tschausepp_history', JSON.stringify(history));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('History too large, trimming...');
|
|
||||||
history.games = history.games.slice(-50);
|
|
||||||
localStorage.setItem('tschausepp_history', JSON.stringify(history));
|
|
||||||
}
|
|
||||||
|
|
||||||
showHistory();
|
|
||||||
switchScreen('setup');
|
|
||||||
document.getElementById('round-number').textContent = '1';
|
|
||||||
gameState.round = 1;
|
|
||||||
gameState.detectedCards = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeScores() {
|
|
||||||
return players.map(p => {
|
|
||||||
const cards = gameState.detectedCards.filter(c => {
|
|
||||||
const idx = Math.floor(
|
|
||||||
Math.atan2(c.y - centerY, c.x - centerX) < 0
|
|
||||||
? Math.atan2(c.y - centerY, c.x - centerX) + 2 * Math.PI
|
|
||||||
: Math.atan2(c.y - centerY, c.x - centerX)
|
|
||||||
) / ((2 * Math.PI) / players.length);
|
|
||||||
return players[idx % players.length] === p;
|
|
||||||
});
|
|
||||||
const total = cards.reduce((s, c) => {
|
|
||||||
return s + cardValues[Object.keys(cardValues)[Math.floor(Math.random() * cardValues.length)]];
|
|
||||||
}, 0);
|
|
||||||
return { name: p, total };
|
|
||||||
}).sort((a, b) => a.total - b.total);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHistory() {
|
|
||||||
const history = JSON.parse(localStorage.getItem('tschausepp_history') || '{}');
|
|
||||||
if (!history.games || history.games.length === 0) {
|
|
||||||
historyList.innerHTML = '<p style="text-align: center; color: #7f8c8d;">No games yet.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
historyList.innerHTML = history.games.map(g => `
|
|
||||||
<div class="history-item">
|
|
||||||
<h3>Game: ${g.players.length} players, ${g.rounds} rounds (${new Date(g.date).toLocaleDateString()})</h3>
|
|
||||||
<div class="history-scores">${JSON.stringify(g.finalScores)}</div>
|
|
||||||
<div class="history-date">Score: ${JSON.stringify(g.finalScores)}</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
initSetup();
|
|
||||||
renderPlayers(['Player 1', 'Player 2']);
|
|
||||||
renderSuitConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue