This commit is contained in:
Pierre Wessman 2025-10-01 14:12:40 +00:00
parent 92b2d30d2b
commit 1990d98aa2
4 changed files with 239 additions and 1 deletions

110
CLAUDE.md Normal file
View File

@ -0,0 +1,110 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
A hockey match engine simulation built with PhaserJS and TypeScript. The engine uses continuous positioning (exact X/Y coordinates on a 2D rink) combined with behavior trees for AI decision-making to create a realistic hockey match simulation.
**Core Concept**: Players have exact positions on a 60x30m international hockey rink, with AI running behavior trees at 60 FPS to make tactical decisions (passing, shooting, positioning, etc.).
## Development Commands
```bash
# Start development server (runs on port 3000)
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
```
## Coordinate System
**Critical**: The rink uses a centered coordinate system:
- Origin (0, 0) = center of rink
- Rink dimensions: 60m length × 30m width
- Left goal: x = -26m, Right goal: x = +26m
- All positions use **meters as floats** (not integers)
- Screen rendering: 1 meter = 20 pixels (SCALE constant)
## Architecture
### Project Structure
```
src/
config/
constants.ts # All game constants (rink dimensions, colors, speeds)
game/
main.ts # Phaser game initialization and config
GameScene.ts # Main game scene (rendering, game loop)
Goal.ts # Goal structure with physics bodies
entities/ # Player and Puck classes (planned)
systems/ # BehaviorTree, PositioningSystem, PuckSystem (planned)
```
### Implementation Phases (from PLAN.md)
The project follows a phased approach:
1. **Phase 1** (✓): Environment setup, rink rendering
2. **Phase 2** (Next): Player entities (12 players: 10 skaters + 2 goalies) with smooth movement
3. **Phase 3**: Puck entity and possession mechanics
4. **Phase 4**: Behavior tree for puck carrier decisions (shoot/pass/carry)
5. **Phase 5**: Team offensive positioning system
6. **Phase 6**: Defensive behaviors and pressure mechanics
7. **Phase 7**: Goalie AI
8. **Phase 8**: Game flow (goals, faceoffs, scoring)
9. **Phase 9**: Polish and tuning
### Key Systems (Planned)
**Behavior Trees**: Decision-making engine that runs every tick
- Evaluates game state (possession, positions, threats)
- Weights actions by player attributes (Hockey IQ, Skill, Speed)
- Outputs: move, pass, shoot, or defensive actions
**Tactical Positioning**: Heat map-based positioning
- Different formations based on game situation (offense/defense)
- Players move toward ideal positions modified by:
- Puck location
- Teammate spacing
- Opponent positions
- Player stamina/speed
**Puck Movement**:
- Pass success = f(passer skill, distance, pressure, receiver skill)
- Shots use trajectory calculation with goalie save probability
- Smooth interpolation for visual feedback
## Technical Details
- **Framework**: Phaser 3.90.0 (with Arcade Physics)
- **TypeScript**: Strict mode enabled
- **Build Tool**: Vite 5.4
- **Target FPS**: 60 (constant in constants.ts)
- **Physics**: Arcade physics with zero gravity (top-down view)
## Configuration
All magic numbers and game constants are centralized in `src/config/constants.ts`:
- Rink dimensions and zone lines
- Goal dimensions
- Colors (ice, boards, lines)
- Scale factor (meters to pixels)
- Game settings (FPS)
Future constants will include:
- Player speeds, shot speeds, pass speeds
- AI decision probabilities
- Attribute ranges
## Development Notes
- **Incremental testing**: Test each phase thoroughly before proceeding
- **Debug visualization**: Add toggleable overlays for targets, zones, etc.
- **Tuning over precision**: Numbers are starting points; tune based on feel
- Refer to PLAN.md for detailed phase requirements and validation criteria
- Refer to NOTES.md for architectural decisions and design notes

112
src/entities/Puck.ts Normal file
View File

@ -0,0 +1,112 @@
import Phaser from 'phaser';
import { SCALE } from '../config/constants';
export type PuckState = 'loose' | 'carried' | 'passing' | 'shot';
export class Puck extends Phaser.GameObjects.Container {
private sprite!: Phaser.GameObjects.Graphics;
private body!: Phaser.Physics.Arcade.Body;
// Position in game coordinates (meters)
public gameX: number;
public gameY: number;
// Puck state
public state: PuckState;
public possession: 'home' | 'away' | null;
public carrier: string | null; // PlayerID
constructor(scene: Phaser.Scene, gameX: number = 0, gameY: number = 0) {
// Convert game coordinates to screen coordinates
const screenX = (scene.game.config.width as number) / 2 + gameX * SCALE;
const screenY = (scene.game.config.height as number) / 2 - gameY * SCALE;
super(scene, screenX, screenY);
this.gameX = gameX;
this.gameY = gameY;
this.state = 'loose';
this.possession = null;
this.carrier = null;
// Add to scene
scene.add.existing(this);
scene.physics.add.existing(this);
this.body = this.body as Phaser.Physics.Arcade.Body;
// Set physics body size to match the puck visual (10px diameter)
this.body.setSize(10, 10);
this.body.setOffset(-5, -5); // Center the body on the container
// Set initial velocity (5 m/s in a random direction for testing)
const angle = 3.1415; //Math.random() * Math.PI * 2;
const speed = 30 * SCALE; // 5 m/s converted to pixels/s
this.body.setVelocity(
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
// Add bounce to keep it moving
this.body.setBounce(1, 1);
this.body.setCollideWorldBounds(true);
this.createSprite();
}
private createSprite() {
const graphics = this.scene.add.graphics();
// Draw puck as black circle (5px radius)
graphics.fillStyle(0x000000, 1);
graphics.fillCircle(0, 0, 5);
// Add white highlight for visibility on ice
graphics.fillStyle(0xffffff, 0.3);
graphics.fillCircle(-1, -1, 2);
this.add(graphics);
this.sprite = graphics;
}
/**
* Update puck position in game coordinates (meters)
*/
public setGamePosition(gameX: number, gameY: number) {
this.gameX = gameX;
this.gameY = gameY;
// Convert to screen coordinates
const centerX = (this.scene.game.config.width as number) / 2;
const centerY = (this.scene.game.config.height as number) / 2;
this.setPosition(
centerX + gameX * SCALE,
centerY - gameY * SCALE
);
}
/**
* Set puck to be carried by a player
*/
public setCarrier(playerId: string, team: 'home' | 'away') {
this.carrier = playerId;
this.possession = team;
this.state = 'carried';
}
/**
* Set puck to loose state
*/
public setLoose() {
this.carrier = null;
this.state = 'loose';
}
/**
* Get puck position in game coordinates
*/
public getGamePosition(): { x: number; y: number } {
return { x: this.gameX, y: this.gameY };
}
}

View File

@ -15,10 +15,12 @@ import {
COLOR_GOAL_CREASE
} from '../config/constants';
import { Goal } from './Goal';
import { Puck } from '../entities/Puck';
export class GameScene extends Phaser.Scene {
private leftGoal!: Goal;
private rightGoal!: Goal;
private puck!: Puck;
constructor() {
super({ key: 'GameScene' });
@ -27,6 +29,20 @@ export class GameScene extends Phaser.Scene {
create() {
this.drawRink();
this.createGoals();
this.createPuck();
}
private createPuck() {
// Initialize puck at center ice (0, 0 in game coordinates)
this.puck = new Puck(this, 0, 0);
// Add collisions between puck and goal posts
this.physics.add.collider(this.puck, this.leftGoal.getLeftPost());
this.physics.add.collider(this.puck, this.leftGoal.getRightPost());
this.physics.add.collider(this.puck, this.leftGoal.getBackBar());
this.physics.add.collider(this.puck, this.rightGoal.getLeftPost());
this.physics.add.collider(this.puck, this.rightGoal.getRightPost());
this.physics.add.collider(this.puck, this.rightGoal.getBackBar());
}
private createGoals() {

View File

@ -12,7 +12,7 @@ const config: Phaser.Types.Core.GameConfig = {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 0 },
debug: false
debug: true
}
},
fps: {