diff --git a/AGENTS.md b/AGENTS.md index 750c3c8..79f8526 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,46 +1,26 @@ # Tschausepp - Jass Card Tracker -## Project Overview -Mobile-first React web app for tracking Schaffhauser (Jass) card game rounds using camera and feature matching to detect and assign cards to players. +Mobile-first React app for tracking Schaffhauser (Jass) card game rounds via camera. ## Tech Stack -- React 18 (lightweight runtime) -- TensorFlow.js (lite mode, CDN-hosted, ~5MB) -- HTML5 Camera API (Mobile-friendly) -- localStorage (JSON format, max 100 games) +- React 18, TypeScript, Vite +- TensorFlow.js (lite mode, CDN-hosted) +- HTML5 Camera API (environment-facing preference) +- localStorage (JSON, max 100 games) -## Key Implementation Details +## Developer Commands +- `npm run typecheck`: Run TypeScript check +- `npm run build`: Build production version +- `npm run preview`: Preview production build +- **Dev Server**: Always running on `localhost:5173`. Do not start manually. -### Architecture -- Component-based structure with 5 main screens: - 1. Setup Screen - Player management and game configuration - 2. Camera Screen - Live preview with card detection overlay - 3. Detection Component - Card boundary detection using image processing - 4. Assignment Component - Radial sector layout with auto-assignment - 5. Results Screen - Score calculation and game results - 6. History Screen - Game storage and export functionality +## Architecture & Logic +- **Screens**: Setup → Camera (with Detection/Assignment) → Results → History. +- **Detection**: Custom image processing + color analysis for suits (Schellen, Schilten, Eicheln, Rosen). +- **Assignment**: Uses radial sectors to assign detected cards to the nearest player. +- **Storage**: Max 100 games in localStorage; follows `games → rounds → cards` structure. -### Development Commands -- `npm run build` - Build production version -- `npm run preview` - Preview production build -- `npm run typecheck` - Run TypeScript type checking - -### Development Server -- A development server is always guaranteed to be running on localhost:5173 -- **Do not start the development server manually under any circumstances** - -### Camera & Detection -- Uses HTML5 Camera API with environment-facing camera preference -- Card detection implemented with custom image processing techniques -- Suit identification (Schellen, Schilten, Eicheln, Rosen) via color analysis -- Auto-assignment based on radial sector + nearest player calculation - -### Data Storage -- Games stored in localStorage with maximum of 100 games -- Data structure follows: games → rounds → cards with player assignments -- JSON format for export functionality - -### Mobile-First Design -- Responsive UI optimized for mobile devices -- Touch-friendly controls and interfaces -- Camera access optimized for mobile environment \ No newline at end of file +## Reference Docs +- `ML_SETUP_GUIDE.md`: ML pipeline architecture, training, and TF.js deployment. +- `DETECTION_IMPROVEMENT_PLAN.md`: Roadmap for improving detection stability and accuracy. +- `swiss_jass_suits.md`: Domain reference for Jass suit colors and iconography. diff --git a/src/components/Camera/CameraScreen.tsx b/src/components/Camera/CameraScreen.tsx index f009c77..8ac3e24 100644 --- a/src/components/Camera/CameraScreen.tsx +++ b/src/components/Camera/CameraScreen.tsx @@ -1,5 +1,5 @@ import React, { useRef, useEffect } from 'react'; -import { GameState } from '../../types'; +import { GameState, Card, Player } from '../../types'; import { useGameStateContext } from '../../context/GameStateContext'; import Detection from '../Detection/Detection'; import Assignment from '../Assignment/Assignment'; @@ -35,17 +35,17 @@ const CameraScreen: React.FC = () => { startCamera(); - return () => { - if (gameState.cameraStream) { - gameState.cameraStream.getTracks().forEach(track => track.stop()); - } - }; + return () => { + if (gameState.cameraStream) { + gameState.cameraStream.getTracks().forEach((track: MediaStreamTrack) => track.stop()); + } + }; }, []); const [showDebug, setShowDebug] = React.useState(false); - const [liveCards, setLiveCards] = React.useState([]); - - const handleCardsDetected = (cards: any[]) => { + const [liveCards, setLiveCards] = React.useState([]); + + const handleCardsDetected = (cards: Card[]) => { scanTable(cards); setShowDebug(true); }; @@ -55,7 +55,7 @@ const CameraScreen: React.FC = () => { (window as any).detectCards(); } else { // Fallback if the global detection function is not yet available - const detectedCards = [ + const detectedCards: Card[] = [ { id: '1', suit: 'Schellen', @@ -121,7 +121,7 @@ const CameraScreen: React.FC = () => { pointerEvents: 'none', zIndex: 10 }}> - {(showDebug ? liveCards : gameState.detectedCards).map((card) => ( + {(showDebug ? liveCards : gameState.detectedCards).map((card: Card) => (
{ {/* Placeholder for radial sectors visualization */} {gameState.players.length > 0 && (
- {gameState.players.map((player, index) => { + {gameState.players.map((player: Player, index: number) => { const angle = (index * 2 * Math.PI) / gameState.players.length; const radius = 150; const x = 320 + radius * Math.cos(angle); diff --git a/src/components/History/HistoryScreen.tsx b/src/components/History/HistoryScreen.tsx index 4625446..334308b 100644 --- a/src/components/History/HistoryScreen.tsx +++ b/src/components/History/HistoryScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Game } from '../../types'; +import { Game, Player } from '../../types'; import { useGameStateContext } from '../../context/GameStateContext'; const HistoryScreen: React.FC = () => { @@ -12,7 +12,7 @@ const HistoryScreen: React.FC = () => { return (
- {gameState.gameHistory.map((game, index) => ( + {gameState.gameHistory.map((game: Game, index: number) => (
{
{Object.entries(game.finalScores).map(([playerId, score]) => ( - {game.players.find(p => p.id === playerId)?.name}: {score} + {game.players.find((p: Player) => p.id === playerId)?.name}: {score} ))}
diff --git a/src/components/Results/ResultsScreen.tsx b/src/components/Results/ResultsScreen.tsx index c2f26ef..6d37b2c 100644 --- a/src/components/Results/ResultsScreen.tsx +++ b/src/components/Results/ResultsScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { GameState } from '../../types'; +import { GameState, Player, Card } from '../../types'; import { useGameStateContext } from '../../context/GameStateContext'; const ResultsScreen: React.FC = () => { @@ -13,15 +13,15 @@ const ResultsScreen: React.FC = () => { // Scoring logic based on card values const scores: Record = {}; - gameState.players.forEach(player => { + gameState.players.forEach((player: Player) => { scores[player.id] = 0; }); // Assign cards to players with proper scoring - gameState.detectedCards.forEach(card => { + gameState.detectedCards.forEach((card: Card) => { // Use the assignedTo property if available, otherwise distribute randomly const player = card.assignedTo - ? gameState.players.find(p => p.id === card.assignedTo) + ? gameState.players.find((p: Player) => p.id === card.assignedTo) : gameState.players[Math.floor(Math.random() * gameState.players.length)]; if (player) { @@ -63,7 +63,7 @@ const ResultsScreen: React.FC = () => {

🔍 Round Results

- {gameState.players.map(player => ( + {gameState.players.map((player: Player) => (

{player.name}

@@ -78,7 +78,7 @@ const ResultsScreen: React.FC = () => {

Detected Cards

- {gameState.detectedCards.map(card => ( + {gameState.detectedCards.map((card: Card) => (
{card.suit === 'Schellen' && '🔔'} diff --git a/src/components/Setup/SetupScreen.tsx b/src/components/Setup/SetupScreen.tsx index 6961b4c..d37a62f 100644 --- a/src/components/Setup/SetupScreen.tsx +++ b/src/components/Setup/SetupScreen.tsx @@ -54,7 +54,7 @@ const SetupScreen: React.FC = () => {

Setup a New Game

- {gameState.players.map((player, index) => ( + {gameState.players.map((player: Player, index: number) => (

👤 {index + 1}. {player.name}

diff --git a/src/integration-tests/tfjs.test.ts b/src/integration-tests/tfjs.test.ts index da14aad..aa4d787 100644 --- a/src/integration-tests/tfjs.test.ts +++ b/src/integration-tests/tfjs.test.ts @@ -1,4 +1,7 @@ // Simple integration test for TensorFlow.js +import * as tf from '@tensorflow/tfjs'; + +// This file would be used in testing or for a simple model load test // This file would be used in testing or for a simple model load test // Not needed in production but demonstrates how we would test the TensorFlow integration diff --git a/src/types/index.ts b/src/types/index.ts index 89cb230..fce9bea 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,6 +12,7 @@ export interface Card { width: number; height: number; confidence: number; + assignedTo?: string; } export interface Round {