fix: resolve all typechecking errors across codebase
This commit is contained in:
parent
73beedee5f
commit
3f3123c5af
7 changed files with 45 additions and 61 deletions
58
AGENTS.md
58
AGENTS.md
|
|
@ -1,46 +1,26 @@
|
||||||
# Tschausepp - Jass Card Tracker
|
# Tschausepp - Jass Card Tracker
|
||||||
|
|
||||||
## Project Overview
|
Mobile-first React app for tracking Schaffhauser (Jass) card game rounds via camera.
|
||||||
Mobile-first React web app for tracking Schaffhauser (Jass) card game rounds using camera and feature matching to detect and assign cards to players.
|
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
- React 18 (lightweight runtime)
|
- React 18, TypeScript, Vite
|
||||||
- TensorFlow.js (lite mode, CDN-hosted, ~5MB)
|
- TensorFlow.js (lite mode, CDN-hosted)
|
||||||
- HTML5 Camera API (Mobile-friendly)
|
- HTML5 Camera API (environment-facing preference)
|
||||||
- localStorage (JSON format, max 100 games)
|
- 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
|
## Architecture & Logic
|
||||||
- Component-based structure with 5 main screens:
|
- **Screens**: Setup → Camera (with Detection/Assignment) → Results → History.
|
||||||
1. Setup Screen - Player management and game configuration
|
- **Detection**: Custom image processing + color analysis for suits (Schellen, Schilten, Eicheln, Rosen).
|
||||||
2. Camera Screen - Live preview with card detection overlay
|
- **Assignment**: Uses radial sectors to assign detected cards to the nearest player.
|
||||||
3. Detection Component - Card boundary detection using image processing
|
- **Storage**: Max 100 games in localStorage; follows `games → rounds → cards` structure.
|
||||||
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
|
|
||||||
|
|
||||||
### Development Commands
|
## Reference Docs
|
||||||
- `npm run build` - Build production version
|
- `ML_SETUP_GUIDE.md`: ML pipeline architecture, training, and TF.js deployment.
|
||||||
- `npm run preview` - Preview production build
|
- `DETECTION_IMPROVEMENT_PLAN.md`: Roadmap for improving detection stability and accuracy.
|
||||||
- `npm run typecheck` - Run TypeScript type checking
|
- `swiss_jass_suits.md`: Domain reference for Jass suit colors and iconography.
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import { GameState } from '../../types';
|
import { GameState, Card, Player } from '../../types';
|
||||||
import { useGameStateContext } from '../../context/GameStateContext';
|
import { useGameStateContext } from '../../context/GameStateContext';
|
||||||
import Detection from '../Detection/Detection';
|
import Detection from '../Detection/Detection';
|
||||||
import Assignment from '../Assignment/Assignment';
|
import Assignment from '../Assignment/Assignment';
|
||||||
|
|
@ -35,17 +35,17 @@ const CameraScreen: React.FC = () => {
|
||||||
|
|
||||||
startCamera();
|
startCamera();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (gameState.cameraStream) {
|
if (gameState.cameraStream) {
|
||||||
gameState.cameraStream.getTracks().forEach(track => track.stop());
|
gameState.cameraStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [showDebug, setShowDebug] = React.useState(false);
|
const [showDebug, setShowDebug] = React.useState(false);
|
||||||
const [liveCards, setLiveCards] = React.useState<any[]>([]);
|
const [liveCards, setLiveCards] = React.useState<Card[]>([]);
|
||||||
|
|
||||||
const handleCardsDetected = (cards: any[]) => {
|
const handleCardsDetected = (cards: Card[]) => {
|
||||||
scanTable(cards);
|
scanTable(cards);
|
||||||
setShowDebug(true);
|
setShowDebug(true);
|
||||||
};
|
};
|
||||||
|
|
@ -55,7 +55,7 @@ const CameraScreen: React.FC = () => {
|
||||||
(window as any).detectCards();
|
(window as any).detectCards();
|
||||||
} else {
|
} else {
|
||||||
// Fallback if the global detection function is not yet available
|
// Fallback if the global detection function is not yet available
|
||||||
const detectedCards = [
|
const detectedCards: Card[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
suit: 'Schellen',
|
suit: 'Schellen',
|
||||||
|
|
@ -121,7 +121,7 @@ const CameraScreen: React.FC = () => {
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
zIndex: 10
|
zIndex: 10
|
||||||
}}>
|
}}>
|
||||||
{(showDebug ? liveCards : gameState.detectedCards).map((card) => (
|
{(showDebug ? liveCards : gameState.detectedCards).map((card: Card) => (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -146,7 +146,7 @@ const CameraScreen: React.FC = () => {
|
||||||
{/* Placeholder for radial sectors visualization */}
|
{/* Placeholder for radial sectors visualization */}
|
||||||
{gameState.players.length > 0 && (
|
{gameState.players.length > 0 && (
|
||||||
<div className="radial-sectors">
|
<div className="radial-sectors">
|
||||||
{gameState.players.map((player, index) => {
|
{gameState.players.map((player: Player, index: number) => {
|
||||||
const angle = (index * 2 * Math.PI) / gameState.players.length;
|
const angle = (index * 2 * Math.PI) / gameState.players.length;
|
||||||
const radius = 150;
|
const radius = 150;
|
||||||
const x = 320 + radius * Math.cos(angle);
|
const x = 320 + radius * Math.cos(angle);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Game } from '../../types';
|
import { Game, Player } from '../../types';
|
||||||
import { useGameStateContext } from '../../context/GameStateContext';
|
import { useGameStateContext } from '../../context/GameStateContext';
|
||||||
|
|
||||||
const HistoryScreen: React.FC = () => {
|
const HistoryScreen: React.FC = () => {
|
||||||
|
|
@ -12,7 +12,7 @@ const HistoryScreen: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="history-list">
|
<div className="history-list">
|
||||||
{gameState.gameHistory.map((game, index) => (
|
{gameState.gameHistory.map((game: Game, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className="history-item"
|
className="history-item"
|
||||||
|
|
@ -28,7 +28,7 @@ const HistoryScreen: React.FC = () => {
|
||||||
<div className="history-scores">
|
<div className="history-scores">
|
||||||
{Object.entries(game.finalScores).map(([playerId, score]) => (
|
{Object.entries(game.finalScores).map(([playerId, score]) => (
|
||||||
<span key={playerId} className="score-tag">
|
<span key={playerId} className="score-tag">
|
||||||
{game.players.find(p => p.id === playerId)?.name}: {score}
|
{game.players.find((p: Player) => p.id === playerId)?.name}: {score}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { GameState } from '../../types';
|
import { GameState, Player, Card } from '../../types';
|
||||||
import { useGameStateContext } from '../../context/GameStateContext';
|
import { useGameStateContext } from '../../context/GameStateContext';
|
||||||
|
|
||||||
const ResultsScreen: React.FC = () => {
|
const ResultsScreen: React.FC = () => {
|
||||||
|
|
@ -13,15 +13,15 @@ const ResultsScreen: React.FC = () => {
|
||||||
// Scoring logic based on card values
|
// Scoring logic based on card values
|
||||||
const scores: Record<string, number> = {};
|
const scores: Record<string, number> = {};
|
||||||
|
|
||||||
gameState.players.forEach(player => {
|
gameState.players.forEach((player: Player) => {
|
||||||
scores[player.id] = 0;
|
scores[player.id] = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assign cards to players with proper scoring
|
// 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
|
// Use the assignedTo property if available, otherwise distribute randomly
|
||||||
const player = card.assignedTo
|
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)];
|
: gameState.players[Math.floor(Math.random() * gameState.players.length)];
|
||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
|
|
@ -63,7 +63,7 @@ const ResultsScreen: React.FC = () => {
|
||||||
<h2>🔍 Round Results</h2>
|
<h2>🔍 Round Results</h2>
|
||||||
|
|
||||||
<div className="results-container">
|
<div className="results-container">
|
||||||
{gameState.players.map(player => (
|
{gameState.players.map((player: Player) => (
|
||||||
<div key={player.id} className="player-result">
|
<div key={player.id} className="player-result">
|
||||||
<h3>{player.name}</h3>
|
<h3>{player.name}</h3>
|
||||||
<div className="score">
|
<div className="score">
|
||||||
|
|
@ -78,7 +78,7 @@ const ResultsScreen: React.FC = () => {
|
||||||
<div className="cards-summary">
|
<div className="cards-summary">
|
||||||
<h3>Detected Cards</h3>
|
<h3>Detected Cards</h3>
|
||||||
<div className="cards-grid">
|
<div className="cards-grid">
|
||||||
{gameState.detectedCards.map(card => (
|
{gameState.detectedCards.map((card: Card) => (
|
||||||
<div key={card.id} className="card-preview">
|
<div key={card.id} className="card-preview">
|
||||||
<div className="card-suit">
|
<div className="card-suit">
|
||||||
{card.suit === 'Schellen' && '🔔'}
|
{card.suit === 'Schellen' && '🔔'}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const SetupScreen: React.FC = () => {
|
||||||
<h2>Setup a New Game</h2>
|
<h2>Setup a New Game</h2>
|
||||||
|
|
||||||
<div className="player-slots">
|
<div className="player-slots">
|
||||||
{gameState.players.map((player, index) => (
|
{gameState.players.map((player: Player, index: number) => (
|
||||||
<div key={player.id} className="player-slot">
|
<div key={player.id} className="player-slot">
|
||||||
<h3>👤 {index + 1}. {player.name}</h3>
|
<h3>👤 {index + 1}. {player.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
// Simple integration test for TensorFlow.js
|
// 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
|
// 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
|
// Not needed in production but demonstrates how we would test the TensorFlow integration
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface Card {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
confidence: number;
|
confidence: number;
|
||||||
|
assignedTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Round {
|
export interface Round {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue