mini-world / gameState.ts
victor's picture
victor HF Staff
Initial deployment of Mini World game
a2d0320 verified
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;
}
}