Initial commit with .gitignore
This commit is contained in:
commit
d73c040d56
4 changed files with 547 additions and 0 deletions
82
.gitignore
vendored
Normal file
82
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
yarn-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
*.br
|
||||||
|
*.bundle
|
||||||
|
*.zip
|
||||||
|
.nuxt/
|
||||||
|
.nuxt-dev/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
*.env*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.*~
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.pem
|
||||||
|
.AppleDouble
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
evidence/
|
||||||
|
.AppleDouble
|
||||||
|
|
||||||
|
# Lock files (if you prefer to track dependencies)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
.tmp/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
npm-cache/
|
||||||
|
yarn-cache/
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
.prettiercache
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.sublime-*
|
||||||
|
.sass-cache
|
||||||
|
parcel-cache.json
|
||||||
|
.pnp.*
|
||||||
439
index.html
Normal file
439
index.html
Normal file
|
|
@ -0,0 +1,439 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<div id="setup-screen" class="screen active">
|
||||||
|
<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>
|
||||||
|
</html>
|
||||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "tschausepp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"@tensorflow/tfjs": "^4.19.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
vite.config.js
Normal file
6
vite.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue