| 1 | /** |
| 2 | * An agent's face: a little creature drawn from its `avatar_seed`, in the |
| 3 | * 5×7 pixel style of the g1t logo. The same seed always gives the same |
| 4 | * creature: symmetric, in one colour from a palette that reads on the dark |
| 5 | * base, and sometimes with eyes. Pure, so it is tested on its own. |
| 6 | */ |
| 7 | |
| 8 | /** Colours that sit well on --g1t-bg, each with a darker shade for its ground. */ |
| 9 | export const CREATURE_COLOURS: { fg: string; ground: string }[] = [ |
| 10 | { fg: "#b6a8ff", ground: "#221e38" }, // lavender |
| 11 | { fg: "#86efc4", ground: "#10302a" }, // mint |
| 12 | { fg: "#ffbd8c", ground: "#35241a" }, // peach |
| 13 | { fg: "#8ab4ff", ground: "#18233a" }, // sky |
| 14 | { fg: "#ff9ecf", ground: "#36192a" }, // pink |
| 15 | { fg: "#f2dc72", ground: "#302a12" }, // lemon |
| 16 | { fg: "#7fe3e0", ground: "#112f30" }, // teal |
| 17 | { fg: "#ff8f85", ground: "#381a19" }, // coral |
| 18 | ]; |
| 19 | |
| 20 | export const COLUMNS = 5; |
| 21 | export const ROWS = 7; |
| 22 | |
| 23 | /** A cell: empty, body, or an eye. */ |
| 24 | export type Cell = 0 | 1 | 2; |
| 25 | |
| 26 | export type Creature = { cells: Cell[][]; colour: { fg: string; ground: string } }; |
| 27 | |
| 28 | /** FNV-1a, 32 bits: the seed as a number. */ |
| 29 | function hash(text: string): number { |
| 30 | let h = 0x811c9dc5; |
| 31 | for (let i = 0; i < text.length; i++) { |
| 32 | h ^= text.charCodeAt(i); |
| 33 | h = Math.imul(h, 0x01000193); |
| 34 | } |
| 35 | return h >>> 0; |
| 36 | } |
| 37 | |
| 38 | /** mulberry32: a small, good-enough generator from one number. */ |
| 39 | function generator(seed: number): () => number { |
| 40 | let a = seed; |
| 41 | return () => { |
| 42 | a = (a + 0x6d2b79f5) | 0; |
| 43 | let t = Math.imul(a ^ (a >>> 15), 1 | a); |
| 44 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; |
| 45 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296; |
| 46 | }; |
| 47 | } |
| 48 | |
| 49 | /** The creature for a seed: the left three columns chosen, mirrored to five. */ |
| 50 | export function creature(seed: string): Creature { |
| 51 | const random = generator(hash(seed || "agent")); |
| 52 | // The first draws of a generator from near seeds sit close together: |
| 53 | // spent, and the colour taken from a hash of its own. |
| 54 | for (let i = 0; i < 4; i++) random(); |
| 55 | const colour = CREATURE_COLOURS[(hash(`colour:${seed}`) >>> 11) % CREATURE_COLOURS.length]!; |
| 56 | const cells: Cell[][] = Array.from({ length: ROWS }, () => Array<Cell>(COLUMNS).fill(0)); |
| 57 | for (let row = 0; row < ROWS; row++) { |
| 58 | for (let column = 0; column < 3; column++) { |
| 59 | // Fuller towards the middle, so it reads as a body, not noise. |
| 60 | const density = column === 2 ? 0.78 : column === 1 ? 0.6 : 0.42; |
| 61 | const filled: Cell = random() < density ? 1 : 0; |
| 62 | cells[row]![column] = filled; |
| 63 | cells[row]![COLUMNS - 1 - column] = filled; |
| 64 | } |
| 65 | } |
| 66 | // A spine down the middle from the second row to the second last, so it holds together. |
| 67 | for (let row = 1; row < ROWS - 1; row++) cells[row]![2] = 1; |
| 68 | // Eyes, more often than not: a pair on the second or third row, in a filled face. |
| 69 | if (random() < 0.7) { |
| 70 | const row = random() < 0.5 ? 2 : 3; |
| 71 | for (const column of [1, 3]) cells[row]![column] = 2; |
| 72 | for (const column of [0, 1, 2, 3, 4]) if (cells[row]![column] === 0 && column !== 0 && column !== 4) cells[row]![column] = 1; |
| 73 | // A brow above the eyes keeps them from floating off the top. |
| 74 | if (row === 2) for (const column of [1, 2, 3]) cells[1]![column] = 1; |
| 75 | } |
| 76 | return { cells, colour }; |
| 77 | } |