From 65e5dc2b8fde339b12cf6864a70873041c87d3c2 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 10 Jun 2025 00:41:00 -0400 Subject: [PATCH] Implement Globetrotter game rules and mechanics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes: - Replace simple two-player game with full Globetrotter ruleset - Add proper game state with Rovers, Buildings, and Fortifications - Implement HQ spaces (pentagons) and regular spaces (triangles) - Rovers start in random HQ spaces with 5 hit points - Movement restricted to adjacent faces only - Add collision detection and fortification blocking - Implement win/loss conditions - Update UI to show game status and unit counts Game mechanics implemented: - Turn-based movement between adjacent faces - Unit positioning on 3D polyhedron - Face type detection (HQ vs regular) - Basic win condition checking Not yet implemented: - Combat system (shooting) - Building construction - Fortification placement - Special building abilities Based on rules from: https://github.com/imjasonh/globetrotter 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 67 +++++++---- game.ts | 319 ++++++++++++++++++++++++++++++++++++++++------------- index.html | 8 +- 3 files changed, 289 insertions(+), 105 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2e14dca..bd8752b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # Claude Code Context ## Project Overview -A web-based 3D snub dodecahedron game built with Three.js and TypeScript. The snub dodecahedron is rendered with its mathematically correct 92 faces (12 pentagons + 80 triangles). +Globetrotter - A 3D strategy game for 2+ players played on a snub dodecahedron planet. Players control Rovers to explore, conquer, and defend territories while building structures and engaging in tactical combat. ## Key Learnings @@ -25,16 +25,17 @@ A web-based 3D snub dodecahedron game built with Three.js and TypeScript. The sn ### Current Features - ✅ Mathematically accurate snub dodecahedron with 92 faces +- ✅ 12 Pentagon spaces serve as HQ locations +- ✅ 80 Triangle spaces for regular gameplay - ✅ Interactive rotation with mouse drag - ✅ Face selection and highlighting -- ✅ Different colors for triangles (gray) and pentagons (darker gray) -- ✅ No visible edges (wireframe opacity = 0) -- ✅ Bright lighting on all sides -- ✅ Fully opaque faces -- ✅ Two-player turn-based game (red vs green) -- ✅ Players can only move to adjacent faces -- ✅ Collision detection (players cannot occupy same face) -- ✅ Game state serialization for save/load functionality +- ✅ Two-player turn-based strategy (red vs green) +- ✅ Rovers with 5 hit points each +- ✅ Movement to adjacent faces only +- ✅ Collision detection (units cannot stack) +- ✅ Enemy fortifications block movement +- ✅ Win/loss conditions implemented +- ✅ Game state serialization for save/load ### Development Commands - Build: `npm run build` or `npx tsc` @@ -46,22 +47,42 @@ A web-based 3D snub dodecahedron game built with Three.js and TypeScript. The sn - @types/three (dev) - polyhedra (for geometry data) -## Game Rules -- Two players (Red and Green) take turns moving -- Players start at random positions on the polyhedron -- On each turn, a player can move to any adjacent face (sharing an edge) -- Players cannot move to faces occupied by the other player -- Click on an adjacent face to move there +## Game Rules (Globetrotter) + +### Setup +- Each player starts with one Rover in a random HQ (pentagon) space +- Rovers have 5 hit points + +### On Your Turn (Choose One Action) +1. **Move**: Move a Rover to an adjacent space +2. **Fortify**: Place a fortification (not yet implemented) +3. **Shoot**: Attack enemy units within 3 spaces (not yet implemented) +4. **Build**: Construct buildings in HQ spaces (not yet implemented) +5. **Destroy**: Remove HQ buildings (not yet implemented) + +### Movement Rules +- Rovers move 1 space to adjacent faces +- Cannot move through enemy fortifications +- Cannot stack units on same space + +### Victory Conditions +- Eliminate all enemy Rovers AND they have no Factory to build new ones +- If all players lose simultaneously, everyone loses +- Planet destruction (8+ drill cannon shots) ends game ## API Methods - `exportGameState()`: Returns serialized game data for saving - `importGameState(data)`: Loads a saved game state -## Next Steps -- Add win conditions -- Add move history visualization -- Add undo/redo functionality -- Add network multiplayer support -- Add AI opponent -- Add animations for piece movement -- Add sound effects \ No newline at end of file +## Not Yet Implemented +- Fortification placement and mechanics +- Shooting/combat system (range calculation, dice rolls) +- Building construction (Space Port, Factory, Drill Cannon, Treasury) +- Special building abilities +- Action point storage (Treasury) +- Rover production (Factory) +- Orbital mechanics (Space Port) +- Drill Cannon charging/firing +- Multi-rover selection UI +- Visual indicators for valid moves +- Sound effects and animations \ No newline at end of file diff --git a/game.ts b/game.ts index 0dd6e07..55704bf 100644 --- a/game.ts +++ b/game.ts @@ -2,6 +2,17 @@ const GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; const XI_CONSTANT = 0.94315125924; // Real zero of x³ + 2x² - φ² = 0 +// Game constants +const GAME_CONSTANTS = { + ROVER_MAX_HP: 5, + BUILDING_MAX_HP: 5, + FORTIFICATION_HP: 1, + SHOOT_RANGE: 3, + HQ_RANGE_COST: 2, + DRILL_CANNON_PLANET_DESTROY_THRESHOLD: 8, + TREASURY_MAX_POINTS: 3 +}; + // Three.js types - global declaration for CDN usage declare const THREE: any; @@ -69,15 +80,45 @@ interface SnubDodecahedronData { // Game state types type Player = 'red' | 'green'; +type SpaceType = 'triangle' | 'pentagon'; +type BuildingType = 'spaceport' | 'factory' | 'drillcannon' | 'treasury'; + +interface Unit { + player: Player; + hitPoints: number; + maxHitPoints: number; +} + +interface Rover extends Unit { + type: 'rover'; + faceId: number; +} + +interface Building extends Unit { + type: 'building'; + buildingType: BuildingType; + faceId: number; +} + +interface Fortification { + type: 'fortification'; + player: Player; + faceId: number; + hitPoints: number; +} interface GameState { currentPlayer: Player; - playerPositions: { + rovers: Rover[]; + buildings: Building[]; + fortifications: Fortification[]; + moveHistory: string[]; + gameStarted: boolean; + actionPointsStored: { red: number; green: number; }; - moveHistory: number[]; - gameStarted: boolean; + drillCannonShots: number; } // Serializable game data for network/save @@ -308,6 +349,16 @@ class SnubDodecahedronGeometry { return adjacencyMap; } + + static getFaceType(faceId: number): SpaceType { + // First 80 faces are triangles, next 12 are pentagons (HQ spaces) + return faceId < 80 ? 'triangle' : 'pentagon'; + } + + static getHQSpaces(): number[] { + // Pentagon faces start at index 80 + return Array.from({ length: 12 }, (_, i) => 80 + i); + } } // Utility functions (pure and testable) @@ -364,10 +415,7 @@ class GeometryUtils { // UI utilities class UIUtils { static updateFaceInfo(faceId: number, type: string): void { - const faceInfoElement = document.getElementById('face-info'); - if (faceInfoElement) { - faceInfoElement.textContent = `Face: ${faceId} (${type})`; - } + // Don't update during game - UI is used for game info } static clearFaceInfo(): void { @@ -400,7 +448,9 @@ class SnubDodecahedronGame { // Game state private gameState: GameState; private faceAdjacencyMap: Map>; - private playerMeshes: Map = new Map(); + private roverMeshes: Map = new Map(); + private buildingMeshes: Map = new Map(); + private fortificationMeshes: Map = new Map(); constructor(config: GameConfig = DEFAULT_CONFIG) { this.config = config; @@ -413,12 +463,16 @@ class SnubDodecahedronGame { // Initialize game state this.gameState = { currentPlayer: 'red', - playerPositions: { - red: Math.floor(Math.random() * 92), - green: Math.floor(Math.random() * 92) - }, + rovers: [], + buildings: [], + fortifications: [], moveHistory: [], - gameStarted: false + gameStarted: false, + actionPointsStored: { + red: 0, + green: 0 + }, + drillCannonShots: 0 }; // Build adjacency map @@ -434,8 +488,7 @@ class SnubDodecahedronGame { this.createSnubDodecahedron(); this.setupLighting(); this.setupEventListeners(); - this.createPlayerPieces(); - this.startGame(); + this.setupGame(); this.animate(); } @@ -657,90 +710,177 @@ class SnubDodecahedronGame { } } - private createPlayerPieces(): void { - // Create spheres for player pieces - const sphereGeometry = new THREE.SphereGeometry(0.1, 32, 16); - - // Red player - const redMaterial = new THREE.MeshLambertMaterial({ - color: this.config.colors.playerRed - }); - const redSphere = new THREE.Mesh(sphereGeometry, redMaterial); - this.playerMeshes.set('red', redSphere); - this.scene.add(redSphere); - - // Green player - const greenMaterial = new THREE.MeshLambertMaterial({ - color: this.config.colors.playerGreen - }); - const greenSphere = new THREE.Mesh(sphereGeometry, greenMaterial); - this.playerMeshes.set('green', greenSphere); - this.scene.add(greenSphere); - - // Position players at their starting faces - this.updatePlayerPositions(); + private setupGame(): void { + // Get available HQ spaces + const hqSpaces = SnubDodecahedronGeometry.getHQSpaces(); + + // Randomly select starting HQ for each player + const shuffled = [...hqSpaces].sort(() => Math.random() - 0.5); + + // Create initial rovers for each player + const redRover: Rover = { + type: 'rover', + player: 'red', + faceId: shuffled[0], + hitPoints: GAME_CONSTANTS.ROVER_MAX_HP, + maxHitPoints: GAME_CONSTANTS.ROVER_MAX_HP + }; + + const greenRover: Rover = { + type: 'rover', + player: 'green', + faceId: shuffled[1], + hitPoints: GAME_CONSTANTS.ROVER_MAX_HP, + maxHitPoints: GAME_CONSTANTS.ROVER_MAX_HP + }; + + this.gameState.rovers.push(redRover, greenRover); + + // Create visual representations + this.createRoverMesh(redRover); + this.createRoverMesh(greenRover); + + // Update positions + this.updateAllUnitPositions(); + + // Start the game + this.gameState.gameStarted = true; + this.updateUI(); } - - private updatePlayerPositions(): void { - ['red', 'green'].forEach((player) => { - const playerKey = player as Player; - const position = this.gameState.playerPositions[playerKey]; - const mesh = this.playerMeshes.get(playerKey); - - if (mesh && this.faces[position]) { - // Get center of face - const face = this.faces[position]; - const center = new THREE.Vector3(); - face.geometry.computeBoundingBox(); - face.geometry.boundingBox.getCenter(center); - face.localToWorld(center); - - // Offset slightly outward from face - const normal = center.clone().normalize(); - center.add(normal.multiplyScalar(0.1)); - - mesh.position.copy(center); + + private createRoverMesh(rover: Rover): void { + const geometry = new THREE.ConeGeometry(0.15, 0.3, 6); + const material = new THREE.MeshLambertMaterial({ + color: rover.player === 'red' ? this.config.colors.playerRed : this.config.colors.playerGreen + }); + const mesh = new THREE.Mesh(geometry, material); + this.roverMeshes.set(rover, mesh); + this.scene.add(mesh); + } + + private updateAllUnitPositions(): void { + // Update rover positions + this.gameState.rovers.forEach(rover => { + const mesh = this.roverMeshes.get(rover); + if (mesh) { + this.positionUnitOnFace(mesh, rover.faceId); + } + }); + + // Update building positions + this.gameState.buildings.forEach(building => { + const mesh = this.buildingMeshes.get(building); + if (mesh) { + this.positionUnitOnFace(mesh, building.faceId); } }); } - + + private positionUnitOnFace(mesh: any, faceId: number): void { + if (this.faces[faceId]) { + const face = this.faces[faceId]; + const center = new THREE.Vector3(); + face.geometry.computeBoundingBox(); + face.geometry.boundingBox.getCenter(center); + face.localToWorld(center); + + // Offset slightly outward from face + const normal = center.clone().normalize(); + center.add(normal.multiplyScalar(0.1)); + + mesh.position.copy(center); + } + } + private attemptMove(targetFaceId: number): void { - const currentPlayer = this.gameState.currentPlayer; - const currentPosition = this.gameState.playerPositions[currentPlayer]; - + // Find the current player's rovers + const currentRovers = this.gameState.rovers.filter(r => r.player === this.gameState.currentPlayer); + + if (currentRovers.length === 0) { + console.log('No rovers to move'); + return; + } + + // For now, move the first rover (later we'll need UI to select which rover) + const rover = currentRovers[0]; + // Check if move is valid (adjacent face) - const adjacentFaces = this.faceAdjacencyMap.get(currentPosition); + const adjacentFaces = this.faceAdjacencyMap.get(rover.faceId); if (!adjacentFaces || !adjacentFaces.has(targetFaceId)) { console.log('Invalid move: not adjacent'); return; } - - // Check if face is occupied by other player - const otherPlayer: Player = currentPlayer === 'red' ? 'green' : 'red'; - if (this.gameState.playerPositions[otherPlayer] === targetFaceId) { + + // Check if face is occupied by unit or building + const occupiedByRover = this.gameState.rovers.some(r => r.faceId === targetFaceId); + const occupiedByBuilding = this.gameState.buildings.some(b => b.faceId === targetFaceId); + + if (occupiedByRover || occupiedByBuilding) { console.log('Invalid move: face occupied'); return; } - + + // Check for enemy fortifications + const enemyFortification = this.gameState.fortifications.find( + f => f.faceId === targetFaceId && f.player !== this.gameState.currentPlayer + ); + + if (enemyFortification) { + console.log('Invalid move: enemy fortification blocks movement'); + return; + } + // Make the move - this.gameState.playerPositions[currentPlayer] = targetFaceId; - this.gameState.moveHistory.push(targetFaceId); - this.updatePlayerPositions(); - + rover.faceId = targetFaceId; + this.gameState.moveHistory.push(`${this.gameState.currentPlayer} moved rover to ${targetFaceId}`); + this.updateAllUnitPositions(); + // Switch players - this.gameState.currentPlayer = otherPlayer; - this.updateUI(); + this.endTurn(); } - - private startGame(): void { - this.gameState.gameStarted = true; + + private endTurn(): void { + this.gameState.currentPlayer = this.gameState.currentPlayer === 'red' ? 'green' : 'red'; this.updateUI(); + this.checkWinConditions(); + } + + private checkWinConditions(): void { + const redAlive = this.gameState.rovers.some(r => r.player === 'red') || + this.gameState.buildings.some(b => b.player === 'red' && b.buildingType === 'factory'); + const greenAlive = this.gameState.rovers.some(r => r.player === 'green') || + this.gameState.buildings.some(b => b.player === 'green' && b.buildingType === 'factory'); + + if (!redAlive && !greenAlive) { + alert('Game Over: Both players eliminated!'); + this.gameState.gameStarted = false; + } else if (!redAlive) { + alert('Game Over: Green wins!'); + this.gameState.gameStarted = false; + } else if (!greenAlive) { + alert('Game Over: Red wins!'); + this.gameState.gameStarted = false; + } + + // Check planet destruction + if (this.gameState.drillCannonShots >= GAME_CONSTANTS.DRILL_CANNON_PLANET_DESTROY_THRESHOLD) { + alert('Game Over: Planet destroyed by drill cannon!'); + this.gameState.gameStarted = false; + } } private updateUI(): void { const info = document.getElementById('face-info'); if (info) { - info.textContent = `Current player: ${this.gameState.currentPlayer.toUpperCase()}`; + const currentRovers = this.gameState.rovers.filter(r => r.player === this.gameState.currentPlayer); + const roverCount = currentRovers.length; + const buildingCount = this.gameState.buildings.filter(b => b.player === this.gameState.currentPlayer).length; + + info.innerHTML = ` + Current Turn: ${this.gameState.currentPlayer.toUpperCase()}
+ Rovers: ${roverCount} | Buildings: ${buildingCount}
+ Action Points: ${this.gameState.actionPointsStored[this.gameState.currentPlayer]} + `; } } @@ -754,8 +894,31 @@ class SnubDodecahedronGame { } public importGameState(data: SerializedGameData): void { + // Clear existing meshes + this.roverMeshes.forEach((mesh, rover) => { + this.scene.remove(mesh); + }); + this.buildingMeshes.forEach((mesh, building) => { + this.scene.remove(mesh); + }); + this.fortificationMeshes.forEach((mesh, fort) => { + this.scene.remove(mesh); + }); + + this.roverMeshes.clear(); + this.buildingMeshes.clear(); + this.fortificationMeshes.clear(); + + // Load new state this.gameState = { ...data.state }; - this.updatePlayerPositions(); + + // Recreate visual elements + this.gameState.rovers.forEach(rover => { + this.createRoverMesh(rover); + }); + + // Update positions and UI + this.updateAllUnitPositions(); this.updateUI(); } diff --git a/index.html b/index.html index e0a7078..94b59b3 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Snub Dodecahedron Game + Globetrotter - 3D Strategy Game