export const emojis = { tree: "🌳", rock: "🪨", character: "🚶", blank: "⬜", diamond: "💎", }; type World = { width: number; height: number; data: string[]; }; type GameObject = { x: number; y: number; emoji: string; }; type Character = GameObject & { inventory: GameObject[]; }; export class GameState { private world: World; private character: Character; private objects: GameObject[] = []; private controllers: Set> = new Set(); private history: any[] = []; constructor() { this.initializeGame(); } private initializeGame() { const worldWidth = 600; const worldHeight = 600; this.world = this.createWorld(worldWidth, worldHeight); this.world = this.generateTerrain( this.world, [ { emoji: "🌳", threshold: 0.4 }, { emoji: "🪨", threshold: 0.5 }, ], 0.1 ); const emptyLocation = this.getRandomEmptyLocation(this.world); this.character = { ...emptyLocation, emoji: emojis.character, inventory: [], }; // Spawn 500 diamonds at random locations for (let i = 0; i < 500; i++) { this.spawnAtRandom(emojis.diamond); } } setController(controller: ReadableStreamDefaultController) { this.controllers.add(controller); try { // Send initial state immediately when controller is set this.sendUpdate(); } catch (error) { console.error("Error sending initial state:", error); this.controllers.delete(controller); } } removeController(controller: ReadableStreamDefaultController) { this.controllers.delete(controller); } getState() { return { map: this.renderAround(this.world, [this.character, ...this.objects], 6), inventory: this.character.inventory.map((item) => item.emoji), history: this.history, }; } async handleAction(action: any) { try { if (action.action === "move") { this.moveObject(this.character, action.detail, this.world); this.history.push({ role: "assistant", content: JSON.stringify(action), }); this.history.push({ role: "user", content: `valid action - input your next action in JSON.\nCurrent map:\n\n${this.renderAround( this.world, [this.character, ...this.objects], 5 )}\n\nInventory: ${this.character.inventory .map((item) => item.emoji) .join(", ")}`, }); } else if (action.action === "pick") { this.pickObject(action.detail); this.history.push({ role: "assistant", content: JSON.stringify(action), }); this.history.push({ role: "user", content: `valid action - input your next action in JSON.\nCurrent map:\n\n${this.renderAround( this.world, [this.character, ...this.objects], 3 )}\n\nInventory: ${this.character.inventory .map((item) => item.emoji) .join(", ")}`, }); } } catch (err) { this.history.push({ role: "user", content: "This action is invalid. Please try something else.", }); } this.sendUpdate(); } private sendUpdate() { if (this.controllers.size === 0) return; const state = this.getState(); const data = `data: ${JSON.stringify(state)}\n\n`; for (const controller of this.controllers) { try { controller.enqueue(data); } catch (error) { console.error("Error sending update to client:", error); this.controllers.delete(controller); } } } // Helper functions moved from main.js private createWorld(width: number, height: number): World { return { width, height, data: new Array(width * height).fill(emojis.blank), }; } private getIndex(world: World, x: number, y: number): number { return y * world.width + x; } private setTile(world: World, x: number, y: number, tile: string): World { const newWorld = { ...world }; newWorld.data[this.getIndex(newWorld, x, y)] = tile; return newWorld; } private getTile(world: World, x: number, y: number): string { return world.data[this.getIndex(world, x, y)]; } private isEmpty(world: World, x: number, y: number): boolean { if (x < 0 || y < 0 || x >= world.width || y >= world.height) { return false; } return this.getTile(world, x, y) === emojis.blank; } private getRandomEmptyLocation(world: World) { let x = Math.floor(Math.random() * world.width); let y = Math.floor(Math.random() * world.height); while (!this.isEmpty(world, x, y)) { x = Math.floor(Math.random() * world.width); y = Math.floor(Math.random() * world.height); } return { x, y }; } private moveObject(object: Character, direction: string, world: World) { const moveMapping: Record = { up: { dx: 0, dy: -1 }, down: { dx: 0, dy: 1 }, left: { dx: -1, dy: 0 }, right: { dx: 1, dy: 0 }, }; const movement = moveMapping[direction]; if ( movement && this.isEmpty(world, object.x + movement.dx, object.y + movement.dy) ) { object.x += movement.dx; object.y += movement.dy; } else { throw new Error("invalid action"); } } private generateTerrain( world: World, emojiThresholds: Array<{ emoji: string; threshold: number }>, noiseScale: number ): World { const permutations = this.generatePermutations(); let newWorld = { ...world }; for (let y = 0; y < newWorld.height; y++) { for (let x = 0; x < newWorld.width; x++) { const noiseValue = (this.perlin2d(x * noiseScale, y * noiseScale, permutations) + 1) / 2; let found = false; for (const { emoji, threshold } of emojiThresholds) { if (noiseValue < threshold) { newWorld = this.setTile(newWorld, x, y, emoji); found = true; break; } } if (!found) { newWorld = this.setTile(newWorld, x, y, emojis.blank); } } } return newWorld; } private generatePermutations(): number[] { const p = new Array(256); for (let i = 0; i < 256; i++) { p[i] = Math.floor(Math.random() * 256); } return p.concat(p); } private fade(t: number): number { return t * t * t * (t * (t * 6 - 15) + 10); } private lerp(t: number, a: number, b: number): number { return a + t * (b - a); } private grad(hash: number, x: number, y: number): number { const h = hash & 3; const u = h < 2 ? x : y; const v = h < 2 ? y : x; return (h & 1 ? -u : u) + (h & 2 ? -2 * v : 2 * v); } private perlin2d(x: number, y: number, permutations: number[]): number { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); const u = this.fade(x); const v = this.fade(y); const A = permutations[X] + Y; const B = permutations[X + 1] + Y; return this.lerp( v, this.lerp( u, this.grad(permutations[A], x, y), this.grad(permutations[B], x - 1, y) ), this.lerp( u, this.grad(permutations[A + 1], x, y - 1), this.grad(permutations[B + 1], x - 1, y - 1) ) ); } private renderAround( world: World, objects: GameObject[], distance: number ): string { if (!objects.length) return "No objects to render"; const mainObject = objects[0]; // Use first object (character) as reference point console.log( `Rendering around character at (${mainObject.x}, ${mainObject.y}) with ${objects.length} total objects` ); const startPosition = { x: mainObject.x - distance, y: mainObject.y - distance, }; const areaWidth = 2 * distance + 1; const areaHeight = 2 * distance + 1; return this.render(world, objects, startPosition, areaWidth, areaHeight); } public spawnObject(x: number, y: number, emoji: string): GameObject { if (!this.isEmpty(this.world, x, y)) { throw new Error("Cannot spawn object on non-empty space"); } const newObject: GameObject = { x, y, emoji }; this.objects.push(newObject); this.sendUpdate(); return newObject; } public spawnAtRandom(emoji: string): GameObject { const location = this.getRandomEmptyLocation(this.world); const obj = this.spawnObject(location.x, location.y, emoji); console.log( `Spawned ${emoji} at (${obj.x}, ${obj.y}). Total objects: ${this.objects.length}` ); return obj; } private getAdjacentObject( x: number, y: number, direction: string ): GameObject | null { const moveMapping: Record = { up: { dx: 0, dy: -1 }, down: { dx: 0, dy: 1 }, left: { dx: -1, dy: 0 }, right: { dx: 1, dy: 0 }, }; const movement = moveMapping[direction]; if (!movement) return null; const targetX = x + movement.dx; const targetY = y + movement.dy; return ( this.objects.find((obj) => obj.x === targetX && obj.y === targetY) || null ); } private pickObject(direction: string) { const object = this.getAdjacentObject( this.character.x, this.character.y, direction ); if (!object) { throw new Error("No object found in that direction"); } // Add to inventory this.character.inventory.push(object); // Remove from world objects this.objects = this.objects.filter((obj) => obj !== object); } private render( world: World, objects: Character[] = [], startPosition = { x: 0, y: 0 }, areaWidth = world.width, areaHeight = world.height ): string { let output = ""; for (let y = startPosition.y; y < startPosition.y + areaHeight; y++) { for (let x = startPosition.x; x < startPosition.x + areaWidth; x++) { if (x >= 0 && y >= 0 && x < world.width && y < world.height) { const objectAtPosition = objects.find( (obj) => obj.x === x && obj.y === y ); if (objectAtPosition) { output += objectAtPosition.emoji; } else { output += this.getTile(world, x, y); } } else { output += "🪨"; } } output += "\n"; } return output; } }