Felladrin's picture
Add the simulator files
17fd0c2 verified
export const TAU = Math.PI * 2;
export function lerp(a, b, t) {
return a + (b - a) * t;
}
export function clamp(v, min, max) {
return v < min ? min : v > max ? max : v;
}
export function distance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
export function distanceSq(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return dx * dx + dy * dy;
}
export function randomRange(min, max) {
return min + Math.random() * (max - min);
}
export function randomInt(min, max) {
return Math.floor(randomRange(min, max + 1));
}
export function randomGaussian(mean = 0, std = 1) {
let u = 0,
v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return mean + std * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(TAU * v);
}
export function randomChoice(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
export function normalize(x, y) {
const len = Math.sqrt(x * x + y * y);
if (len === 0) return { x: 0, y: 0 };
return { x: x / len, y: y / len };
}
export function angleBetween(x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1);
}
export function hslToHex(h, s, l) {
s /= 100;
l /= 100;
const a = s * Math.min(l, 1 - l);
const f = (n) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color)
.toString(16)
.padStart(2, "0");
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return { r, g, b };
}
export function rgbToString(r, g, b, a = 1) {
return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`;
}
export function smoothstep(edge0, edge1, x) {
const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
return t * t * (3 - 2 * t);
}
/** Simple seeded pseudo-random for reproducible sequences */
export function seededRandom(seed) {
let s = seed;
return function () {
s = (s * 1664525 + 1013904223) & 0xffffffff;
return (s >>> 0) / 0xffffffff;
};
}
/** Object pool for reducing GC pressure */
export class ObjectPool {
constructor(factory, reset, initialSize = 100) {
this._factory = factory;
this._reset = reset;
this._pool = [];
for (let i = 0; i < initialSize; i++) {
this._pool.push(factory());
}
}
acquire() {
if (this._pool.length > 0) {
return this._pool.pop();
}
return this._factory();
}
release(obj) {
this._reset(obj);
this._pool.push(obj);
}
get available() {
return this._pool.length;
}
}
/** Spatial hash grid for efficient proximity queries */
export class SpatialGrid {
constructor(width, height, cellSize) {
this.cellSize = cellSize;
this.cols = Math.ceil(width / cellSize);
this.rows = Math.ceil(height / cellSize);
this.cells = new Array(this.cols * this.rows);
this.clear();
}
clear() {
for (let i = 0; i < this.cells.length; i++) {
if (this.cells[i]) {
this.cells[i].length = 0;
} else {
this.cells[i] = [];
}
}
}
_key(col, row) {
return row * this.cols + col;
}
insert(item, x, y) {
const col = Math.floor(x / this.cellSize);
const row = Math.floor(y / this.cellSize);
if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) {
this.cells[this._key(col, row)].push(item);
}
}
query(x, y, radius) {
const results = [];
const minCol = Math.max(0, Math.floor((x - radius) / this.cellSize));
const maxCol = Math.min(
this.cols - 1,
Math.floor((x + radius) / this.cellSize),
);
const minRow = Math.max(0, Math.floor((y - radius) / this.cellSize));
const maxRow = Math.min(
this.rows - 1,
Math.floor((y + radius) / this.cellSize),
);
const rSq = radius * radius;
for (let row = minRow; row <= maxRow; row++) {
for (let col = minCol; col <= maxCol; col++) {
const cell = this.cells[this._key(col, row)];
for (let i = 0; i < cell.length; i++) {
const item = cell[i];
const dx = item.x - x;
const dy = item.y - y;
if (dx * dx + dy * dy <= rSq) {
results.push(item);
}
}
}
}
return results;
}
}
/** Exponential moving average for smooth telemetry */
export class EMA {
constructor(alpha = 0.1) {
this.alpha = alpha;
this.value = null;
}
update(v) {
if (this.value === null) {
this.value = v;
} else {
this.value = this.alpha * v + (1 - this.alpha) * this.value;
}
return this.value;
}
reset() {
this.value = null;
}
}
/** Ring buffer for fixed-size history */
export class RingBuffer {
constructor(capacity) {
this.capacity = capacity;
this.data = new Array(capacity);
this.head = 0;
this.size = 0;
}
push(value) {
this.data[this.head] = value;
this.head = (this.head + 1) % this.capacity;
if (this.size < this.capacity) this.size++;
}
toArray() {
const arr = new Array(this.size);
for (let i = 0; i < this.size; i++) {
arr[i] =
this.data[(this.head - this.size + i + this.capacity) % this.capacity];
}
return arr;
}
get last() {
if (this.size === 0) return undefined;
return this.data[(this.head - 1 + this.capacity) % this.capacity];
}
}
/** Color constants for entity types */
export const COLORS = {
gatherer: { hex: "#00ff88", r: 0, g: 255, b: 136 },
predator: { hex: "#ff3366", r: 255, g: 51, b: 102 },
builder: { hex: "#3399ff", r: 51, g: 153, b: 255 },
explorer: { hex: "#ffaa00", r: 255, g: 170, b: 0 },
hybrid: { hex: "#cc66ff", r: 204, g: 102, b: 255 },
food: { hex: "#00ffaa", r: 0, g: 255, b: 170 },
energy: { hex: "#ffee44", r: 255, g: 238, b: 68 },
cyan: { hex: "#00f0ff", r: 0, g: 240, b: 255 },
};
/** Format a number for display (e.g., 1234 -> 1.2k) */
export function formatNumber(n) {
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
if (n >= 1000) return (n / 1000).toFixed(1) + "k";
return Math.round(n).toString();
}
/** Wrap value within bounds */
export function wrap(value, min, max) {
const range = max - min;
return ((((value - min) % range) + range) % range) + min;
}