Spaces:
Paused
Paused
| 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<ReadableStreamDefaultController<string>> = 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<string>) { | |
| 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<string>) { | |
| 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<string, { dx: number; dy: number }> = { | |
| 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<string, { dx: number; dy: number }> = { | |
| 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; | |
| } | |
| } | |