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

11
README Normal file
View file

@ -0,0 +1,11 @@
# Fisher
Fish simulation as a TCP service.
## TODO
- [ ] Colors.
- [ ] Don't redraw if buffer unchanged.
- [ ] Don't overwrite fishes with space.
- [ ] Layer labels above fishes.
- [ ] Post new fishes.

2
assets/fishes/albert.txt Normal file
View file

@ -0,0 +1,2 @@
_
<•_)<(

View file

@ -0,0 +1,2 @@
____ _
/o'_/\(

View file

@ -0,0 +1,2 @@
___ ,
(.o.)<,

2
assets/fishes/daniel.txt Normal file
View file

@ -0,0 +1,2 @@
_
(_\/(

2
assets/fishes/emile.txt Normal file
View file

@ -0,0 +1,2 @@
___
/•v_><[

7
deno.json Normal file
View file

@ -0,0 +1,7 @@
{
"fmt": {
"useTabs": true,
"lineWidth": 120,
"semiColons": false
}
}

78
deno.lock generated Normal file
View file

@ -0,0 +1,78 @@
{
"version": "4",
"specifiers": {
"npm:@spissvinkel/simplex-noise@1.0.1": "1.0.1"
},
"npm": {
"@spissvinkel/simplex-noise@1.0.1": {
"integrity": "sha512-Ni1aqFlajxWFxkp2WuCspat6wmrsRZmDv1hgt90hm8J0XkRByd1bzlJFIKw9kC0q5MlOH5GyVrHVR/ZVvhOzqg=="
}
},
"remote": {
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/bytes/concat.ts": "86161274b5546a02bdb3154652418efe7af8c9310e8d54107a68aaa148e0f5ed",
"https://deno.land/std@0.224.0/bytes/copy.ts": "08d85062240a7223e6ec4e2af193ad1a50c59a43f0d86ac3a7b16f3e0d77c028",
"https://deno.land/std@0.224.0/io/_common.ts": "36705cdb4dfcd338d6131bca1b16e48a4d5bf0d1dada6ce397268e88c17a5835",
"https://deno.land/std@0.224.0/io/_constants.ts": "3c7ad4695832e6e4a32e35f218c70376b62bc78621ef069a4a0a3d55739f8856",
"https://deno.land/std@0.224.0/io/buf_reader.ts": "aa6d589e567c964c8ba1f582648f3feac45e88ab2e3d2cc2c9f84fd73c05d051",
"https://deno.land/std@0.224.0/io/buf_writer.ts": "3fab3fbeae7a6ed1672207b640b6781a2369890878f8a7bb35f95d364dd6dae6",
"https://deno.land/std@0.224.0/io/buffer.ts": "4d1f805f350433e418002accec798bc6c33ce18f614afa65f987c202d7b2234e",
"https://deno.land/std@0.224.0/io/copy.ts": "63c6a4acf71fb1e89f5e47a7b3b2972f9d2c56dd645560975ead72db7eb23f61",
"https://deno.land/std@0.224.0/io/copy_n.ts": "1bad1525b1f78737cb4c11084e2f7cf22359210fea89e2f0e784bd9a4b221fca",
"https://deno.land/std@0.224.0/io/iterate_reader.ts": "1e5e4fea22d8965afb7df4ee9ab9adda0a0fc581adbea31bc2f2d25453f8a6e9",
"https://deno.land/std@0.224.0/io/limited_reader.ts": "bbe3f7dafe9dd076d0c92d88b1b94e91ecad4825997064f44c2a767737ad2526",
"https://deno.land/std@0.224.0/io/mod.ts": "f0a3f9d419394cec27099ba15ab0c8100da1e70928f95ebe646caaf667c6870c",
"https://deno.land/std@0.224.0/io/multi_reader.ts": "dd8f06d50adec0e1befb92a1d354fcf28733a4b1669b23bf534ace161ce61b1c",
"https://deno.land/std@0.224.0/io/read_all.ts": "876c1cb20adea15349c72afc86cecd3573335845ae778967aefb5e55fe5a8a4a",
"https://deno.land/std@0.224.0/io/read_delim.ts": "d26cfed53b0c2c127ec3453ccd65f474ecc36a331209e246448885434afffc4e",
"https://deno.land/std@0.224.0/io/read_int.ts": "2412f106103a271f5d770b2a61c97b13eebcd18de414a0a092ed82dfcb875903",
"https://deno.land/std@0.224.0/io/read_lines.ts": "17b96f87dbebc5edb976b3d83dc7713bddb994e9c8cb688012d6c6c26803fb9e",
"https://deno.land/std@0.224.0/io/read_long.ts": "d7cd367ab5c1263014c32a76afa0f0584c3bf3a3d860963c7368a6825094df46",
"https://deno.land/std@0.224.0/io/read_range.ts": "2ddfedbfff44e4ea8d60c0463cddb2b1860293d932d6a72a0fb78bdf412538fc",
"https://deno.land/std@0.224.0/io/read_short.ts": "f6dff570e685ade917dcb5188e8ecf0b701d6581b0cd186f08e6efe7f5ce33f7",
"https://deno.land/std@0.224.0/io/read_string_delim.ts": "f6beafa8969e6d9d6d7d679f846cd6595d8b6e99091a20a0aecbd50eb30a391e",
"https://deno.land/std@0.224.0/io/reader_from_stream_reader.ts": "a75bbc93f39df8b0e372cc1fbdc416a7cbf2a39fc4c09ddb057f1241100191c5",
"https://deno.land/std@0.224.0/io/slice_long_to_bytes.ts": "bc59a7aaac64845371dbd44debf3e864ae7b7e453127751d96e30adb29fb633b",
"https://deno.land/std@0.224.0/io/string_reader.ts": "279e9ea72e0ed7af6a9cb6da84f4148af93df849d308c31f124ca21f16d09cf2",
"https://deno.land/std@0.224.0/io/string_writer.ts": "923954c2038a622b84c294b94a3a322565fa0d67e8b4c62942b154fc1ad3bb9b",
"https://deno.land/std@0.224.0/io/to_readable_stream.ts": "ed03a44a1ec1cc55a85a857acf6cac472035298f6f3b6207ea209f93b4aefb39",
"https://deno.land/std@0.224.0/io/to_writable_stream.ts": "ef422e0425963c8a1e0481674e66c3023da50f0acbe5ef51ec9789efc3c1e2ed",
"https://deno.land/std@0.224.0/io/types.ts": "acecb3074c730b5ff487ba4fe9ce51e67bd982aa07c95e5f5679b7b2f24ad129",
"https://deno.land/std@0.224.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038",
"https://deno.land/x/noise@v1.1.0/_utils.ts": "09d276f9f2b76b7ed91d0553dfef63af2df911ef252ea831850a3e9d595b3e0c",
"https://deno.land/x/noise@v1.1.0/fractal/_math.ts": "f85d46e7e9781f54f4c6d7f360e8fff52516a0d13933fb550b14993a48cdb3af",
"https://deno.land/x/noise@v1.1.0/fractal/cubic.ts": "08da8567c1b04ba27eb789d3c1933e3ed8d5e58af1d03a278e558191e94954e1",
"https://deno.land/x/noise@v1.1.0/fractal/cylindrical.ts": "f0c324889481f2493f9787bee4059ff4da7b8127797846bbf297a0d10d675a3b",
"https://deno.land/x/noise@v1.1.0/fractal/linear.ts": "7957faa7c16289fae40d53a79a2d287bcb9eee936d0022f82d2e67fb132bb510",
"https://deno.land/x/noise@v1.1.0/fractal/mod.ts": "8a204173bd4eaf71052b61eda3c7eb9be626b983ae3885f659316594e3094596",
"https://deno.land/x/noise@v1.1.0/fractal/options.ts": "48a8c835637855a7fe77a16d38db6c4bd9b5f48912d456d1f45ba1e37e7bdc55",
"https://deno.land/x/noise@v1.1.0/fractal/rectangular.ts": "b11c760f1d0d999461e5ea1cf4d1cd8b08eb819c7029cee3bf9b09330ae79376",
"https://deno.land/x/noise@v1.1.0/fractal/spherical.ts": "a2bdd079f155594259c541238c39b2dde7b74c5a3d0f74449bed579c668bada4",
"https://deno.land/x/noise@v1.1.0/math.ts": "d93ab4c1aa2c45635db3936b0cc4c612e21cdfa7165140bb138d25b4003e0466",
"https://deno.land/x/noise@v1.1.0/mod.ts": "488e92a50b28b5209c8e6c93e736df2f4542adb90f193de1a756093548191dce",
"https://deno.land/x/noise@v1.1.0/noise.ts": "2e937c759f55681fecb17ccf88e08b358df99459115bd9f26a02313b7cf6fb38",
"https://deno.land/x/noise@v1.1.0/open_simplex/2d.ts": "01930f7f1a585c10eed02d6abf548e00ade756ff29f57439c669ad6d67070a43",
"https://deno.land/x/noise@v1.1.0/open_simplex/3d.ts": "610ff2533419ae4252b78584df341617c420a56d3cc663bcf5e32e50babb5899",
"https://deno.land/x/noise@v1.1.0/open_simplex/4d.ts": "7566693f82a35da0643ab4f404e0eca142b990065d60b38f998a842b485912f3",
"https://deno.land/x/noise@v1.1.0/open_simplex/_3d_constants.ts": "7f89ce76d03edc51bd150fbab9036ce439eda76c6ee73b738b16d6a485760b4a",
"https://deno.land/x/noise@v1.1.0/open_simplex/_4d_constants.ts": "fa218f32de12dc8311e0d7bf32cdbe438df2b45058a215340e4527dde714cee4",
"https://deno.land/x/noise@v1.1.0/open_simplex/mod.ts": "084791c4e4e44c9389cfcc6667e4078560349b999f998813a9d309522789021b",
"https://deno.land/x/noise@v1.1.0/options.ts": "6048592a492b84724af37aee34a199d0cd1887ad39edec16bcb27bf5cf2869f9",
"https://deno.land/x/noise@v1.1.0/perlin/2d.ts": "c7a4273d4e6d9ff669aa764c2eca7b990f7e760848cd56fbeceb459f45837d91",
"https://deno.land/x/noise@v1.1.0/perlin/3d.ts": "4575cbcbc3b2c0d368d215e51c3646e97b9ed0c9d8875c9f860d770b673b0533",
"https://deno.land/x/noise@v1.1.0/perlin/4d.ts": "8e878efd0edfb31d3872f33652fcb478b6433fd210630cf4d80e91ef4f219113",
"https://deno.land/x/noise@v1.1.0/perlin/mod.ts": "30a2ce75e4bf9ae74fb08e9de6b4e767f62500cf532508a0fb4dc9a09e4165c8",
"https://deno.land/x/noise@v1.1.0/perlin/options.ts": "af59a2db880352261a57208fae136700bb5ed331439e6da2b49e02a56ea7f6a2",
"https://deno.land/x/noise@v1.1.0/simplex/2d.ts": "d6cdcf77280c55c968af591b0a41bcb42a9495270e84e29fce7d568114d3b82f",
"https://deno.land/x/noise@v1.1.0/simplex/3d.ts": "e8663685cf32b2da4b400cf9504a883a1e77eb95ffff1565570ea3c48486ea16",
"https://deno.land/x/noise@v1.1.0/simplex/4d.ts": "57349d61e0461c509da85c468bd03962fc6dd75c50491b5bbafb918242190b80",
"https://deno.land/x/noise@v1.1.0/simplex/mod.ts": "084791c4e4e44c9389cfcc6667e4078560349b999f998813a9d309522789021b",
"https://deno.land/x/noise@v1.1.0/value/1d.ts": "240b0bde9623ab2bfb41caf9591c60fe3db9dff386fa23651a321ed712acf6b3",
"https://deno.land/x/noise@v1.1.0/value/2d.ts": "f8cef1670aa1559353437c720394519324aa0e6d033c51edabb833af89efd227",
"https://deno.land/x/noise@v1.1.0/value/3d.ts": "878ae8722db4a92386d140b3654246b837f635354fca591a4f5b6611d65cc24b",
"https://deno.land/x/noise@v1.1.0/value/4d.ts": "2c655fc2b4e47459dcd945e5ba1e91dd16fbf6a1638587feda5d6bb46ce21910",
"https://deno.land/x/noise@v1.1.0/value/mod.ts": "0590eb561a46e6495bbe91501db0cdec75756f4080ce64096017bdcfb7cfaf24",
"https://deno.land/x/noise@v1.1.0/value/options.ts": "6b3a0206247410b2f6a5540aedc66819c0b6a92ea53a4856be2da917e8ad6e0c"
}
}

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()