mirror of
https://github.com/imjasonh/dodec
synced 2026-07-07 01:32:17 +00:00
Implement Globetrotter game rules and mechanics
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 <noreply@anthropic.com>
This commit is contained in:
parent
9fe8ec147d
commit
65e5dc2b8f
3 changed files with 289 additions and 105 deletions
67
CLAUDE.md
67
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
|
||||
## 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
|
||||
319
game.ts
319
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<number, Set<number>>;
|
||||
private playerMeshes: Map<Player, any> = new Map();
|
||||
private roverMeshes: Map<Rover, any> = new Map();
|
||||
private buildingMeshes: Map<Building, any> = new Map();
|
||||
private fortificationMeshes: Map<Fortification, any> = 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 = `
|
||||
<strong>Current Turn: ${this.gameState.currentPlayer.toUpperCase()}</strong><br>
|
||||
Rovers: ${roverCount} | Buildings: ${buildingCount}<br>
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Snub Dodecahedron Game</title>
|
||||
<title>Globetrotter - 3D Strategy Game</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
@ -39,10 +39,10 @@
|
|||
<div id="container">
|
||||
<div id="ui">
|
||||
<div id="info">
|
||||
<h3>Snub Dodecahedron Game</h3>
|
||||
<h3>Globetrotter</h3>
|
||||
<p>Rotate: Click and drag</p>
|
||||
<p>Click faces to interact</p>
|
||||
<p id="face-info">Face: None</p>
|
||||
<p>Click adjacent face to move</p>
|
||||
<div id="face-info">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue