omnibot3000 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.prettierrc +18 -0
  2. package/README.md +9 -0
  3. package/api/server.ts +153 -0
  4. package/eslint.config.js +103 -0
  5. package/index.html +22 -0
  6. package/netlify.toml +9 -0
  7. package/nodemon.json +4 -0
  8. package/omnibot3000.code-workspace +55 -0
  9. package/package.json +58 -0
  10. package/pnpm-workspace.yaml +2 -0
  11. package/public/fonts/vt220.woff2 +0 -0
  12. package/src/App.module.css +128 -0
  13. package/src/App.tsx +193 -0
  14. package/src/Error.tsx +80 -0
  15. package/src/commons/OmnibotSpeak.module.css +43 -0
  16. package/src/commons/OmnibotSpeak.tsx +31 -0
  17. package/src/commons/api/api.ts +150 -0
  18. package/src/commons/constants.ts +34 -0
  19. package/src/commons/favicon.ts +69 -0
  20. package/src/commons/hooks/useConfig.tsx +50 -0
  21. package/src/commons/hooks/useKeyPress.tsx +76 -0
  22. package/src/commons/hooks/useStorage.tsx +38 -0
  23. package/src/commons/layout/Background.module.css +47 -0
  24. package/src/commons/layout/Background.tsx +138 -0
  25. package/src/commons/layout/Breadcrumb.module.css +28 -0
  26. package/src/commons/layout/Breadcrumb.tsx +54 -0
  27. package/src/commons/layout/Container.module.css +51 -0
  28. package/src/commons/layout/Container.tsx +60 -0
  29. package/src/commons/layout/Footer.module.css +36 -0
  30. package/src/commons/layout/Footer.tsx +74 -0
  31. package/src/commons/layout/Header.module.css +73 -0
  32. package/src/commons/layout/Header.tsx +102 -0
  33. package/src/commons/layout/Menu.module.css +36 -0
  34. package/src/commons/layout/Menu.tsx +37 -0
  35. package/src/commons/persona.txt +38 -0
  36. package/src/commons/styles/debug.css +71 -0
  37. package/src/commons/styles/main.css +221 -0
  38. package/src/commons/styles/vt220.css +69 -0
  39. package/src/commons/ui/Button.tsx +22 -0
  40. package/src/commons/ui/Caret.tsx +20 -0
  41. package/src/commons/ui/Line.tsx +64 -0
  42. package/src/commons/ui/ScrollSnap.tsx +51 -0
  43. package/src/commons/ui/Separator.tsx +19 -0
  44. package/src/commons/ui/Spacer.tsx +12 -0
  45. package/src/commons/utils/canvas.ts +20 -0
  46. package/src/commons/utils/color.ts +4 -0
  47. package/src/commons/utils/math.ts +43 -0
  48. package/src/commons/utils/strings.ts +47 -0
  49. package/src/commons/utils/styles.ts +11 -0
  50. package/src/commons/utils/system.ts +6 -0
  51. package/src/commons/utils/version.ts +24 -0
  52. package/src/features/chat/Chat.module.css +8 -0
  53. package/src/features/chat/Chat.tsx +188 -0
  54. package/src/features/chat/commons/strings.ts +6 -0
  55. package/src/features/chat/components/Message.module.css +28 -0
  56. package/src/features/chat/components/Message.tsx +45 -0
  57. package/src/features/chat/components/Toolbar.module.css +19 -0
  58. package/src/features/chat/components/Toolbar.tsx +44 -0
  59. package/src/features/chat/hooks/useChatCompletionStore.tsx +160 -0
  60. package/src/features/cli/Cli.module.css +75 -0
  61. package/src/features/cli/Cli.tsx +303 -0
  62. package/src/features/console/cmd.ts +93 -0
  63. package/src/features/console/config.ts +106 -0
  64. package/src/features/help/Help.module.css +8 -0
  65. package/src/features/help/Help.tsx +78 -0
  66. package/src/features/history/History.module.css +77 -0
  67. package/src/features/history/History.tsx +92 -0
  68. package/src/features/home/Home.module.css +26 -0
  69. package/src/features/home/Home.tsx +101 -0
  70. package/src/features/life/Life.module.css +8 -0
  71. package/src/features/life/Life.tsx +16 -0
  72. package/src/features/life/generation.ts +103 -0
  73. package/src/features/life/lifeforms.ts +138 -0
  74. package/src/features/life/types.ts +5 -0
  75. package/src/features/version/Version.module.css +8 -0
  76. package/src/features/version/Version.tsx +70 -0
  77. package/src/global.d.ts +10 -0
  78. package/src/main.tsx +32 -0
  79. package/src/vite-env.d.ts +16 -0
  80. package/tsconfig.api.json +16 -0
  81. package/tsconfig.json +47 -0
  82. package/upgrade.sh +22 -0
  83. package/vite.config.ts +51 -0
@@ -0,0 +1,106 @@
1
+ import {SESSION_KEY, VERSION} from "@commons/constants";
2
+ import {getVariableFromCSS, setVariableToCSS} from "@utils/styles";
3
+
4
+ export interface ConfigType {
5
+ version: string;
6
+ debug: boolean;
7
+ color: {
8
+ h: string;
9
+ s: string;
10
+ l: string;
11
+ };
12
+ size: number;
13
+ height: number;
14
+ }
15
+ export type ConfigParam = "debug" | "color" | "size" | "height";
16
+ export type ConfigValue = boolean | string | number;
17
+
18
+ class Config {
19
+ declare readonly DEFAULT: ConfigType;
20
+ declare config: ConfigType;
21
+
22
+ static readonly KEY: string = `${SESSION_KEY}_config`;
23
+
24
+ constructor() {
25
+ this.DEFAULT = {
26
+ version: VERSION.join("."),
27
+ debug: false,
28
+ color: {
29
+ h: getVariableFromCSS("h"),
30
+ s: getVariableFromCSS("s"),
31
+ l: getVariableFromCSS("l"),
32
+ },
33
+ size: parseInt(getVariableFromCSS("base-size")),
34
+ height: parseFloat(getVariableFromCSS("base-height")),
35
+ };
36
+ this.config = this.read();
37
+ }
38
+
39
+ get configKey(): string {
40
+ return `${SESSION_KEY}_config`;
41
+ }
42
+
43
+ create(): void {
44
+ this.save(this.read());
45
+ }
46
+
47
+ read(): ConfigType {
48
+ let config = JSON.parse(localStorage.getItem(Config.KEY) || "{}");
49
+ if (Object.keys(config).length === 0) config = this.DEFAULT;
50
+ if (config.version !== this.DEFAULT.version) config = this.DEFAULT;
51
+ return config;
52
+ }
53
+
54
+ update(param: ConfigParam, key: string, value: ConfigValue) {
55
+ switch (param) {
56
+ case "debug":
57
+ if (!key && typeof value === "boolean")
58
+ this.config.debug = value as boolean;
59
+ break;
60
+ case "color":
61
+ switch (key) {
62
+ case "h":
63
+ case "s":
64
+ case "l":
65
+ this.config.color[key] = value as string;
66
+ break;
67
+ }
68
+ break;
69
+ case "size":
70
+ case "height":
71
+ if (!key && typeof value === "number")
72
+ this.config[param] = value as number;
73
+ break;
74
+ }
75
+ if (this.config.debug)
76
+ console.info(
77
+ `%cconfig updated: ${param} = ${value}`,
78
+ "color:#999",
79
+ this.config,
80
+ );
81
+ this.save();
82
+ }
83
+
84
+ delete(): void {
85
+ localStorage.removeItem(Config.KEY);
86
+ this.config = this.DEFAULT;
87
+ if (this.config.debug) console.info("%cconfig deleted", "color:#999");
88
+ }
89
+
90
+ save(config?: ConfigType): void {
91
+ localStorage.setItem(Config.KEY, JSON.stringify(config || this.config));
92
+ if (this.config.debug) console.info("%cconfig saved", "color:#999");
93
+ }
94
+
95
+ apply(): void {
96
+ const {color, size, height} = this.config;
97
+ setVariableToCSS("h", color.h);
98
+ setVariableToCSS("s", color.s);
99
+ setVariableToCSS("l", color.l);
100
+ if (size) setVariableToCSS("font-size", `${size}px`);
101
+ if (height) setVariableToCSS("line-height", `${height}rem`);
102
+ if (this.config.debug) console.info("%cconfig applied", "color:#999");
103
+ }
104
+ }
105
+
106
+ export default Config;
@@ -0,0 +1,8 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: start;
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ }
@@ -0,0 +1,78 @@
1
+ import {memo, useEffect, useRef, useState} from "react";
2
+
3
+ import {ChatCompletionMessageParam} from "openai/resources";
4
+ import {ChatCompletionChunk} from "openai/resources/index.mjs";
5
+ import {Stream} from "openai/streaming.mjs";
6
+
7
+ import getData, {getSystemConfig} from "@api/api";
8
+ import OmnibotSpeak from "@commons/OmnibotSpeak";
9
+ import Container from "@layout/Container";
10
+
11
+ import useChatCompletionStore from "@chat/hooks/useChatCompletionStore";
12
+ import styles from "@help/Help.module.css";
13
+
14
+ import cls from "classnames";
15
+
16
+ const Help = () => {
17
+ const chatStore = useChatCompletionStore();
18
+
19
+ const hasRunOnce = useRef(false);
20
+ const [response, setResponse] = useState<string>("");
21
+ const [loading, setLoading] = useState<boolean>(false);
22
+
23
+ const getResponse = async () => {
24
+ try {
25
+ const messages: ChatCompletionMessageParam[] = [getSystemConfig()];
26
+
27
+ messages.push({
28
+ role: "developer",
29
+ content: `\
30
+ make a list of all available config commands. \
31
+ add a description of each command to help the user. \
32
+ you can give a single example for commands that need parameter. \
33
+ highlight the command in bold and keep all comments short.`,
34
+ });
35
+
36
+ const response = (await getData(messages)) as Stream<ChatCompletionChunk>;
37
+
38
+ for await (const chunk of response) {
39
+ const choice = chunk.choices?.[0] || {};
40
+ const finish_reason = choice.finish_reason;
41
+ const text = choice.delta?.content || "";
42
+ if (finish_reason) {
43
+ setLoading(false);
44
+ if (finish_reason === "length")
45
+ setResponse((prev) => `${prev}\n\n[max tokens length reached]\n`);
46
+ break;
47
+ }
48
+ if (!text) continue;
49
+ setResponse((prev) => `${prev}${text}`);
50
+ }
51
+ } catch (error) {
52
+ console.error("Error reading stream:", error);
53
+ setLoading(false);
54
+ setResponse("no signal");
55
+ }
56
+ };
57
+
58
+ useEffect(() => {
59
+ if (hasRunOnce.current) return;
60
+ hasRunOnce.current = true;
61
+
62
+ chatStore.resetChat();
63
+ setLoading(true);
64
+ getResponse();
65
+ }, []);
66
+
67
+ return (
68
+ <div className={styles.root}>
69
+ <Container>
70
+ <div className={cls("text", styles.body)}>
71
+ <OmnibotSpeak truth={response} hasCaret={loading} />
72
+ </div>
73
+ </Container>
74
+ </div>
75
+ );
76
+ };
77
+
78
+ export default memo(Help);
@@ -0,0 +1,77 @@
1
+ .root {
2
+ padding: 0;
3
+ margin: 0;
4
+ margin-right: calc(var(--font-width) - var(--scrollbar-size));
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow-x: hidden;
8
+ overflow-y: scroll;
9
+ scroll-snap-type: both mandatory;
10
+ li {
11
+ list-style-type: none;
12
+ scroll-snap-align: start;
13
+ }
14
+ }
15
+
16
+ .header {
17
+ display: flex;
18
+ flex-direction: row;
19
+ }
20
+
21
+ .toolbar {
22
+ display: flex;
23
+ flex-direction: row;
24
+ transition: opacity var(--duration-fade) ease-out;
25
+ }
26
+
27
+ .line {
28
+ margin-left: calc(var(--font-width) * 2);
29
+ opacity: var(--opacity-tertiary);
30
+ user-select: none;
31
+ cursor: default;
32
+ }
33
+
34
+ .content {
35
+ display: flex;
36
+ flex-direction: row;
37
+ column-gap: var(--font-width);
38
+ }
39
+
40
+ .show {
41
+ opacity: 1;
42
+ transition: opacity var(--duration-fade) ease-out;
43
+ }
44
+
45
+ .hide {
46
+ opacity: 0;
47
+ transition: opacity var(--duration-fade) ease-out;
48
+ }
49
+
50
+ .text {
51
+ text-align: left;
52
+ flex-grow: 1;
53
+ text-wrap: wrap;
54
+ user-select: text;
55
+ }
56
+
57
+ .selected {
58
+ opacity: var(--opacity-primary);
59
+ transition: opacity var(--duration-transition) ease-out;
60
+ }
61
+
62
+ .selected::before {
63
+ content: ">";
64
+ opacity: var(--opacity-secondary);
65
+ transition: opacity var(--duration-transition) ease-out;
66
+ }
67
+
68
+ .not-selected {
69
+ opacity: var(--opacity-secondary);
70
+ transition: opacity var(--duration-transition) ease-out;
71
+ }
72
+
73
+ .not-selected::before {
74
+ content: ">";
75
+ opacity: 0;
76
+ transition: opacity var(--duration-transition) ease-out;
77
+ }
@@ -0,0 +1,92 @@
1
+ import {memo, useEffect} from "react";
2
+ import {useNavigate} from "react-router-dom";
3
+
4
+ import {ASCII_HLINE, BUTTON_DELETE} from "@commons/constants";
5
+ import Button from "@ui/Button";
6
+ import {getVariableFromCSS} from "@utils/styles";
7
+
8
+ import useStorage from "@hooks/useStorage";
9
+
10
+ import type {Chat, ChatId} from "@chat/hooks/useChatCompletionStore";
11
+ import useChatCompletionStore from "@chat/hooks/useChatCompletionStore";
12
+ import styles from "@history/History.module.css";
13
+
14
+ import cls from "classnames";
15
+
16
+ const History = () => {
17
+ const chatStore = useChatCompletionStore();
18
+ const chatId = chatStore.getChatId();
19
+ const storage = useStorage();
20
+
21
+ const navigate = useNavigate();
22
+
23
+ const w = parseInt(getVariableFromCSS("menu-width") ?? 0);
24
+
25
+ const removeChat = (chatId: ChatId) => {
26
+ chatStore.deleteChat(chatId);
27
+ /* if the removed chat is the current one, we reset to a blank chat */
28
+ if (chatId === chatStore.chatId) {
29
+ chatStore.setCompletions();
30
+ chatStore.setChatId();
31
+ }
32
+ storage.save();
33
+ };
34
+
35
+ useEffect(() => {
36
+ const target = document.getElementById(`chat-history-${chatId}`);
37
+ if (target)
38
+ target.scrollIntoView({
39
+ behavior: "smooth",
40
+ block: "nearest",
41
+ });
42
+ }, [chatId]);
43
+
44
+ return (
45
+ <ul className={cls("text", styles.root)}>
46
+ {chatStore
47
+ .getChats()
48
+ .map((chat: Chat) => {
49
+ const selected = Boolean(chatId === chat.id);
50
+ const id = `chat-history-${chat.id}`;
51
+ return (
52
+ <li
53
+ key={id}
54
+ id={id}
55
+ className={styles[chat.title ? "show" : "hide"]}>
56
+ <div
57
+ className={cls(
58
+ styles.content,
59
+ styles[`${selected ? "" : "not-"}selected`],
60
+ )}>
61
+ <button
62
+ className={cls("ascii", "text", styles.text, {
63
+ opacity: selected ? 1 : 0.7,
64
+ })}
65
+ onClick={() => {
66
+ navigate(`/chat/${chat.id}`);
67
+ }}>
68
+ {chat.title}
69
+ </button>
70
+ </div>
71
+ <div className={styles.toolbar}>
72
+ <div className={styles.line}>
73
+ {String(ASCII_HLINE).repeat(w - 3 - BUTTON_DELETE.length)}
74
+ </div>
75
+ <div className={styles.delete}>
76
+ <Button
77
+ name={BUTTON_DELETE}
78
+ handler={() => {
79
+ removeChat(chat.id);
80
+ }}
81
+ />
82
+ </div>
83
+ </div>
84
+ </li>
85
+ );
86
+ })
87
+ .reverse()}
88
+ </ul>
89
+ );
90
+ };
91
+
92
+ export default memo(History);
@@ -0,0 +1,26 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: start;
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ }
9
+
10
+ .button {
11
+ padding-top: var(--line-height);
12
+ }
13
+
14
+ .button::before {
15
+ content: ">";
16
+ display: inline-block;
17
+ padding-right: var(--margin);
18
+ opacity: var(--opacity-tertiary);
19
+ }
20
+
21
+ .button::after {
22
+ content: "<";
23
+ display: inline-block;
24
+ padding-left: var(--margin);
25
+ opacity: var(--opacity-tertiary);
26
+ }
@@ -0,0 +1,101 @@
1
+ import {memo, useEffect, useRef, useState} from "react";
2
+ import {useNavigate} from "react-router-dom";
3
+
4
+ import {ChatCompletionMessageParam} from "openai/resources";
5
+ import {ChatCompletionChunk} from "openai/resources/index.mjs";
6
+ import {Stream} from "openai/streaming.mjs";
7
+
8
+ import getData, {getStartButton, getSystemConfig} from "@api/api";
9
+ import OmnibotSpeak from "@commons/OmnibotSpeak";
10
+ import Container from "@layout/Container";
11
+ import Button from "@ui/Button";
12
+ import {formatText} from "@utils/strings";
13
+
14
+ import useChatCompletionStore from "@chat/hooks/useChatCompletionStore";
15
+ import styles from "@home/Home.module.css";
16
+
17
+ import cls from "classnames";
18
+
19
+ const Home = () => {
20
+ const chatStore = useChatCompletionStore();
21
+
22
+ const navigate = useNavigate();
23
+
24
+ const hasRunOnce = useRef(false);
25
+ const [response, setResponse] = useState<string>("");
26
+ const [loading, setLoading] = useState<boolean>(false);
27
+ const [startButton, setStartButton] = useState<string>("");
28
+
29
+ const updateStartButton = async () => {
30
+ const data = await getStartButton();
31
+ setStartButton(formatText(data));
32
+ };
33
+
34
+ const getResponse = async () => {
35
+ try {
36
+ const messages: ChatCompletionMessageParam[] = [getSystemConfig()];
37
+
38
+ messages.push({
39
+ role: "developer",
40
+ content: `\
41
+ write an intro message for the user. keep it short and to the point. \
42
+ explain who are you, why you are here and how you can help the user. \
43
+ separate each element with an empty line.`,
44
+ });
45
+
46
+ const response = (await getData(messages)) as Stream<ChatCompletionChunk>;
47
+
48
+ for await (const chunk of response) {
49
+ const choice = chunk.choices?.[0] || {};
50
+ const finish_reason = choice.finish_reason;
51
+ const text = choice.delta?.content || "";
52
+ if (finish_reason) {
53
+ setLoading(false);
54
+ if (finish_reason === "length")
55
+ setResponse((prev) => `${prev}\n\n[max tokens length reached]\n`);
56
+ break;
57
+ }
58
+ if (!text) continue;
59
+ setResponse((prev) => `${prev}${text}`);
60
+ }
61
+ } catch (error) {
62
+ console.error("Error reading stream:", error);
63
+ setLoading(false);
64
+ setResponse("no signal");
65
+ }
66
+ };
67
+
68
+ const newChat = () => {
69
+ chatStore.setChatId();
70
+ chatStore.setCompletionId();
71
+ chatStore.setCompletions();
72
+ navigate("/chat");
73
+ };
74
+
75
+ useEffect(() => {
76
+ if (hasRunOnce.current) return;
77
+ hasRunOnce.current = true;
78
+
79
+ chatStore.resetChat();
80
+ setLoading(true);
81
+ getResponse();
82
+ updateStartButton();
83
+ }, []);
84
+
85
+ return (
86
+ <div className={styles.root}>
87
+ <Container>
88
+ <div className={cls("text", styles.body)}>
89
+ <OmnibotSpeak truth={response} hasCaret={loading} />
90
+ {!loading && startButton && (
91
+ <div className={styles.button}>
92
+ <Button name={startButton} handler={newChat} className="text" />
93
+ </div>
94
+ )}
95
+ </div>
96
+ </Container>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default memo(Home);
@@ -0,0 +1,8 @@
1
+ .root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: start;
5
+ width: 100%;
6
+ height: 100%;
7
+ overflow: hidden;
8
+ }
@@ -0,0 +1,16 @@
1
+ import {memo, useEffect, useRef} from "react";
2
+
3
+ import styles from "@life/Life.module.css";
4
+
5
+ const Life = () => {
6
+ const hasRunOnce = useRef(false);
7
+
8
+ useEffect(() => {
9
+ if (hasRunOnce.current) return;
10
+ hasRunOnce.current = true;
11
+ }, []);
12
+
13
+ return <div className={styles.root}></div>;
14
+ };
15
+
16
+ export default memo(Life);
@@ -0,0 +1,103 @@
1
+ import {
2
+ ASCII_CURRENCY,
3
+ ASCII_DOT,
4
+ //ASCII_POINT,
5
+ ASCII_SPACE,
6
+ } from "@commons/constants";
7
+ import {vec2} from "@utils/math";
8
+
9
+ import LIFEFORMS from "@life/lifeforms";
10
+ import {Cell, Grid, Lifeform} from "@life/types";
11
+
12
+ export const init = (w: number, h: number): Grid =>
13
+ Array.from({length: h}, () => Array.from({length: w}, () => 0));
14
+
15
+ export const birth = (
16
+ grid: Grid,
17
+ lifeform: Lifeform,
18
+ x: number,
19
+ y: number,
20
+ w: number,
21
+ h: number,
22
+ ) => {
23
+ lifeform.forEach((v: vec2): void => {
24
+ grid[(y + h + v[1]) % h][(x + w + v[0]) % w] = 1;
25
+ });
26
+ return grid;
27
+ };
28
+
29
+ export const randomize = (
30
+ grid: Grid,
31
+ n: number,
32
+ w: number,
33
+ h: number,
34
+ ): Grid => {
35
+ for (let i = 0; i < n; i++) {
36
+ birth(
37
+ grid,
38
+ LIFEFORMS[Math.round(Math.random() * (LIFEFORMS.length - 1))],
39
+ Math.round(Math.random() * w),
40
+ Math.round(Math.random() * h),
41
+ w,
42
+ h,
43
+ );
44
+ }
45
+ return grid;
46
+ };
47
+
48
+ const getCell = (
49
+ grid: Grid,
50
+ x: number,
51
+ y: number,
52
+ w: number,
53
+ h: number,
54
+ ): Cell => grid[(y + h) % h][(x + w) % w];
55
+
56
+ const countNeighbours = (
57
+ grid: Grid,
58
+ x: number,
59
+ y: number,
60
+ w: number,
61
+ h: number,
62
+ ): number => {
63
+ let count = 0;
64
+ if (getCell(grid, x - 1, y - 1, w, h) === 1) count++;
65
+ if (getCell(grid, x, y - 1, w, h) === 1) count++;
66
+ if (getCell(grid, x + 1, y - 1, w, h) === 1) count++;
67
+ if (getCell(grid, x - 1, y, w, h) === 1) count++;
68
+ if (getCell(grid, x + 1, y, w, h) === 1) count++;
69
+ if (getCell(grid, x - 1, y + 1, w, h) === 1) count++;
70
+ if (getCell(grid, x, y + 1, w, h) === 1) count++;
71
+ if (getCell(grid, x + 1, y + 1, w, h) === 1) count++;
72
+ return count;
73
+ };
74
+
75
+ export const tick = (grid: Grid, w: number, h: number): Grid => {
76
+ const g: Grid = [];
77
+ let population = 0;
78
+ for (let y = 0; y < h; y++) {
79
+ g[y] = [];
80
+ for (let x = 0; x < w; x++) {
81
+ const alive = grid[y][x] === 1;
82
+ const neighbours = countNeighbours(grid, x, y, w, h);
83
+ if (alive) {
84
+ g[y][x] = neighbours < 2 || neighbours > 3 ? 2 : 1;
85
+ } else {
86
+ g[y][x] = neighbours === 3 ? 1 : 0;
87
+ }
88
+ if (g[y][x] === 1) population++;
89
+ }
90
+ }
91
+ if (population === 0) randomize(g, (w * h) / 256, w, h);
92
+ return g;
93
+ };
94
+
95
+ export const render = (grid: Grid, w: number): string =>
96
+ grid
97
+ .flat()
98
+ .map(
99
+ (v, i) =>
100
+ (v === 1 ? ASCII_CURRENCY : v === 0 ? ASCII_SPACE : ASCII_DOT) +
101
+ ((i + 1) % w === 0 ? "\n" : ""),
102
+ )
103
+ .join("");