This commit is contained in:
Matthieu Jolimaitre 2025-05-28 00:20:03 +02:00
commit 011ea8bdeb
9 changed files with 239 additions and 0 deletions

133
src/main.ts Executable file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env -S deno run -A
import { writeAll } from "https://deno.land/std@0.224.0/io/mod.ts"
import { mkSimplexNoise, SimplexNoise } from "npm:@spissvinkel/simplex-noise@1.0.1"
async function main() {
const connections = Listener.listen(8080)
const fishes = await Fishes.load("./assets/fishes")
for await (const moved of fishes.movements()) {
console.log(moved.name, "at", moved.position)
const displayed = esc_clear() + fishes.fishes.map(display).join("") + "\n"
connections.send(displayed)
}
}
class Listener {
private constructor(
private connections: Set<Deno.TcpConn>,
) {}
public static listen(port: number) {
const connections = new Set<Deno.TcpConn>()
;(async () => {
for await (const conn of Deno.listen({ transport: "tcp", port })) connections.add(conn)
})()
return new Listener(connections)
}
public async send(text: string) {
const encoded = new TextEncoder().encode(text)
const promises = new Array<Promise<unknown>>()
for (const conn of this.connections) {
const promise = writeAll(conn, encoded).catch((_) => this.connections.delete(conn))
promises.push(promise)
}
await Promise.all(promises)
}
}
class Fishes {
private constructor(
public fishes: Fish[],
) {}
public static async load(path: string, max = 999) {
const fishes = new Array<Fish>()
for await (const entry of Deno.readDir(path)) {
if (fishes.length >= max) break
if (!entry.isFile) continue
if (!entry.name.endsWith(".txt")) continue
const name = entry.name.slice(0, entry.name.length - ".txt".length)
const file_path = `${path}/${entry.name}`
const content = await Deno.readTextFile(file_path)
const shape = content.split("\n").filter((l) => l.length > 0)
const size: [number, number] = [shape.map((l) => l.length).reduce((a, b) => Math.max(a, b)), shape.length]
fishes.push(new Fish(name, size, shape, [Math.random() * 20, Math.random() * 20]))
}
console.log("Loaded", fishes.length, "fishes.")
return new Fishes(fishes)
}
public async *movements() {
const noise = mkSimplexNoise(Math.random)
while (true) {
const picked = pick(this.fishes)
picked.move_into(noise, [[0, 0], [80, 25]])
yield picked
await new Promise((res) => setTimeout(res, 200))
}
}
}
class Fish {
public constructor(
public name: string,
public size: [x: number, y: number],
public shape: string[],
public position: [number, number] = [0, 0],
public age = 0,
) {}
public move_into(noise: SimplexNoise, bounds: [[number, number], [number, number]]) {
const base = [...this.name].map((c) => c.charCodeAt(0)).reduce((a, b) => a + b)
const velocityX = noise.noise2D(this.age % 100, base)
const velocityY = noise.noise2D(this.age % 100, base + 100)
this.position[0] += velocityX
this.position[1] += velocityY
this.age += 1
if (this.position[0] < bounds[0][0]) this.position[0] = bounds[0][0]
if (this.position[1] < bounds[0][1]) this.position[1] = bounds[0][1]
if (this.position[0] > bounds[1][0]) this.position[0] = bounds[1][0]
if (this.position[1] > bounds[1][1]) this.position[1] = bounds[1][1]
}
}
/**
* ```
* _ <name>
* /
* #####
* #####
* #####
* ```
*
* @param fish Fish to display.
*/
function display(fish: Fish) {
const tick_pad = " ".repeat(fish.size[0])
// TODO : add position and color
return [
tick_pad + " _ " + fish.name,
tick_pad + "/",
...fish.shape,
]
.map((l, i) => esc_goto([fish.position[0], fish.position[1] + i]) + l)
.join("") + "\n"
}
function pick<T>(arr: T[], random = Math.random) {
const index = Math.floor(random() * arr.length)
return arr[index]
}
function esc_goto(pos: [number, number]) {
const [x, y] = pos.map(Math.floor)
return "\u001B[" + y + ";" + x + "H"
}
function esc_clear() {
return "\u001B[3J"
}
if (import.meta.main) await main()