From 1990d98aa2e7dee51879034787a165157899a3e4 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 1 Oct 2025 14:12:40 +0000 Subject: [PATCH] puck 1 --- CLAUDE.md | 110 +++++++++++++++++++++++++++++++++++++++++ src/entities/Puck.ts | 112 ++++++++++++++++++++++++++++++++++++++++++ src/game/GameScene.ts | 16 ++++++ src/game/main.ts | 2 +- 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md create mode 100644 src/entities/Puck.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c7e421f --- /dev/null +++ b/CLAUDE.md @@ -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 diff --git a/src/entities/Puck.ts b/src/entities/Puck.ts new file mode 100644 index 0000000..e63bea4 --- /dev/null +++ b/src/entities/Puck.ts @@ -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 }; + } +} diff --git a/src/game/GameScene.ts b/src/game/GameScene.ts index cfcef24..6ce39c1 100644 --- a/src/game/GameScene.ts +++ b/src/game/GameScene.ts @@ -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() { diff --git a/src/game/main.ts b/src/game/main.ts index 5fbba04..4862273 100644 --- a/src/game/main.ts +++ b/src/game/main.ts @@ -12,7 +12,7 @@ const config: Phaser.Types.Core.GameConfig = { default: 'arcade', arcade: { gravity: { x: 0, y: 0 }, - debug: false + debug: true } }, fps: {