Commit all uncommitted changes

This commit is contained in:
10x Developer 2026-05-04 21:40:32 +02:00
parent 8b00801628
commit 8ecb23b7dc

View file

@ -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> </head>
* { box-sizing: border-box; margin: 0; padding: 0; } <body>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; } <div id="root"></div>
.app { max-width: 800px; margin: 0 auto; padding: 1rem; min-height: 100vh; } <script type="module" src="/src/main.jsx"></script>
.screen { display: none; } </body>
.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> </html>