Skip to content

Commit 204a4e8

Browse files
authored
Merge pull request #63 from healeycodes/post/icepath
add icepath post
2 parents 11fae80 + 7ff9dda commit 204a4e8

File tree

3 files changed

+505
-0
lines changed

3 files changed

+505
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
import { SetStateAction, useEffect, useState } from "react";
2+
3+
const WAIT_TIME = 100;
4+
const FINISH_TIME = 1000;
5+
6+
const helloWorld = `###########################
7+
# @ o | > & ! ± #
8+
# #
9+
# l h #
10+
# > & l & e & ^ #
11+
###########################
12+
`
13+
14+
const fibonacci = `##################################################
15+
# @ > 9 1 + 1 0 | | < #
16+
# > > : ! ~ ; + R 1 - : 0 = R R ^ #
17+
# ± #
18+
##################################################
19+
`
20+
21+
const ladder = `###################
22+
# @ 3 5 8 | #
23+
# > ~ ‖ #
24+
###################
25+
# ‖ R < #
26+
###################
27+
`
28+
29+
export const HelloWorld = () => {
30+
return <IcePath fp="hello.ice" program={helloWorld} />
31+
}
32+
33+
export const Fibonacci = () => {
34+
return <IcePath fp="fibonacci.ice" program={fibonacci} />
35+
}
36+
37+
export const Ladder = () => {
38+
return <IcePath fp="ladder.ice" program={ladder} />
39+
}
40+
41+
const IcePath = ({ fp, program }: { fp: string, program: string }) => {
42+
const [terminal, setTerminal] = useState<React.ReactNode>(null);
43+
useEffect(() => {
44+
let cancelled = false;
45+
(async () => {
46+
while (!cancelled) {
47+
await run(fp, program, () => cancelled, (node) => setTerminal(node));
48+
await new Promise((resolve) => setTimeout(resolve, FINISH_TIME));
49+
}
50+
})();
51+
52+
return () => {
53+
cancelled = true;
54+
}
55+
}, [fp, program]);
56+
57+
return (
58+
<div>{terminal}</div>
59+
);
60+
}
61+
62+
// Coordinates are { x, y }. Access as grid[x][y].
63+
type Cursor = { x: number; y: number; dir: number };
64+
const UP = 0;
65+
const RIGHT = 1;
66+
const DOWN = 2;
67+
const LEFT = 3;
68+
69+
// Search for '@' in program and return the cursor position
70+
function findInit(grid: string[][]): Cursor {
71+
for (let x = 0; x < grid.length; x++) {
72+
for (let y = 0; y < (grid[x] ?? []).length; y++) {
73+
if ((grid[x] ?? [])[y] === "@") {
74+
return { x, y, dir: RIGHT };
75+
}
76+
}
77+
}
78+
throw new Error("No @ found");
79+
}
80+
81+
// Apply direction to cursor
82+
function move(cursor: Cursor): Cursor {
83+
switch (cursor.dir) {
84+
case UP:
85+
cursor.y--;
86+
return cursor;
87+
case RIGHT:
88+
cursor.x++;
89+
return cursor;
90+
case DOWN:
91+
cursor.y++;
92+
return cursor;
93+
case LEFT:
94+
cursor.x--;
95+
return cursor;
96+
}
97+
throw new Error("Invalid direction");
98+
}
99+
100+
function pop(stack: (string | number)[]): string | number {
101+
const top = stack.pop();
102+
if (top === undefined) {
103+
throw new Error("Stack is empty");
104+
}
105+
return top;
106+
}
107+
108+
let lastOp: Record<string, string> = {};
109+
async function render(fp: string, file: string, cursor: Cursor, stack: (string | number)[], stdout: (string | number)[], setTerminal: (nodes: SetStateAction<React.ReactNode[]>) => void) {
110+
const idx = cursor.x + cursor.y * (file.indexOf('\n') + 1);
111+
const op = file[idx] ? opMap[file[idx]] : undefined;
112+
113+
if (op && op !== lastOp[fp]) {
114+
lastOp[fp] = op;
115+
}
116+
117+
const charNodes: React.ReactNode[] = [];
118+
for (let i = 0; i < file.length; i++) {
119+
const ch = file[i];
120+
if (ch === '\n') {
121+
charNodes.push(<br key={`br-${i}`} />);
122+
continue;
123+
}
124+
const isCursor = i === idx;
125+
const isWall = ch === '#';
126+
const classes = `ch${isCursor ? ' hl' : ''}${isWall ? ' wall' : ''}`;
127+
charNodes.push(
128+
<span key={`ch-${i}`} className={classes}>{ch === ' ' ? '\u00A0' : ch}</span>
129+
);
130+
}
131+
132+
const view = (
133+
<div className="terminal">
134+
<div className="grid">{charNodes}</div>
135+
<div className="meta">
136+
<div className="line"><span className="label">Last Op</span><span className="sep">: </span><span className="value">{lastOp[fp] || ''}</span></div>
137+
<div className="line"><span className="label">Stack</span><span className="sep">: </span><span className="value">{stack.join(', ')}</span></div>
138+
<div className="line"><span className="label">Stdout</span><span className="sep">: </span><span className="value">{stdout.join(', ')}</span></div>
139+
<div className="line"><span className="label">Program</span><span className="sep">: </span><span className="value">{fp}</span></div>
140+
</div>
141+
<style jsx>{`
142+
.terminal {
143+
--bg: #111827;
144+
--fg: #e5e7eb;
145+
--dim: #9ca3af;
146+
background: var(--bg);
147+
color: var(--fg);
148+
font-family: "IBM Plex Mono", monospace;
149+
font-size: 14px;
150+
line-height: 1.25;
151+
padding: 12px 14px;
152+
border-radius: 0.4em;
153+
border: 1px solid rgba(255, 255, 255, 0.08);
154+
width: 100%;
155+
max-width: 100%;
156+
overflow: auto;
157+
overscroll-behavior: contain;
158+
}
159+
.grid {
160+
margin-bottom: 8px;
161+
white-space: nowrap;
162+
display: inline-block;
163+
min-width: max-content;
164+
}
165+
.terminal :global(.ch) {
166+
display: inline-block;
167+
width: 1ch;
168+
}
169+
.terminal :global(.wall) {
170+
color: var(--dim);
171+
}
172+
.terminal :global(.hl) {
173+
background: var(--fg);
174+
color: var(--bg);
175+
border-radius: 2px;
176+
}
177+
.terminal :global(.hl.wall) {
178+
color: var(--bg);
179+
}
180+
.meta .line {
181+
margin: 2px 0;
182+
}
183+
.meta .label {
184+
color: var(--dim);
185+
}
186+
`}</style>
187+
</div>
188+
);
189+
190+
setTerminal([view]);
191+
192+
await new Promise((resolve) => setTimeout(resolve, WAIT_TIME));
193+
}
194+
195+
const opMap: Record<string, string> = {
196+
'@': 'START',
197+
'|': 'DOWN',
198+
'>': 'RIGHT',
199+
'<': 'LEFT',
200+
'^': 'UP',
201+
'R': 'ROT',
202+
':': 'DUP',
203+
';': 'DUP2',
204+
'~': 'SWAP',
205+
'=': 'EQ',
206+
'+': 'ADD',
207+
'-': 'SUB',
208+
'&': 'JOIN',
209+
'!': 'PRINT',
210+
'±': 'EXIT',
211+
}
212+
213+
async function run(fp: string, file: string, shouldStop: () => boolean, setTerminal: (nodes: React.ReactNode) => void) {
214+
const rawLines = file.split("\n").map((line) => line.split(""));
215+
const width = Math.max(0, ...rawLines.map((r) => r.length));
216+
const height = rawLines.length;
217+
const grid = Array.from({ length: width }, (_, x) =>
218+
Array.from({ length: height }, (_, y) => rawLines[y]?.[x])
219+
); // grid[x][y]
220+
221+
const stack: (string | number)[] = [];
222+
let cursor: Cursor = findInit(grid);
223+
let stdout: (string | number)[] = [];
224+
225+
let exit = false
226+
while (true) {
227+
if (shouldStop()) {
228+
break;
229+
}
230+
231+
await render(fp, file, cursor, stack, stdout, setTerminal as (nodes: React.SetStateAction<React.ReactNode[]>) => void);
232+
233+
// Make sure to render before exiting
234+
if (exit) {
235+
break;
236+
}
237+
238+
cursor = move(cursor);
239+
const nextChar = grid[cursor.x]?.[cursor.y];
240+
241+
if (nextChar === undefined) {
242+
throw new Error(`Out of bounds x=${cursor.x}, y=${cursor.y}`);
243+
}
244+
245+
if (nextChar === " ") {
246+
continue;
247+
}
248+
249+
// Handle ladder climbing (teleport to other ladder)
250+
if (nextChar === "‖") {
251+
const otherLadder = findOtherLadder(grid, cursor);
252+
cursor.x = otherLadder.x;
253+
cursor.y = otherLadder.y;
254+
continue;
255+
}
256+
257+
// Handle direction change
258+
if (nextChar === "|") {
259+
cursor.dir = DOWN;
260+
continue;
261+
} else if (nextChar === ">") {
262+
cursor.dir = RIGHT;
263+
continue;
264+
} else if (nextChar === "<") {
265+
cursor.dir = LEFT;
266+
continue;
267+
} else if (nextChar === "^") {
268+
cursor.dir = UP;
269+
continue;
270+
}
271+
272+
// ROT (3-element rotation) a b c -> b c a
273+
if (nextChar === "R") {
274+
const a = pop(stack);
275+
const b = pop(stack);
276+
const c = pop(stack);
277+
stack.push(b); // bottom
278+
stack.push(a); // middle
279+
stack.push(c); // top
280+
continue;
281+
}
282+
283+
// Duplicate second to top
284+
if (nextChar === ";") {
285+
const second = stack[stack.length - 2];
286+
if (second === undefined) {
287+
throw new Error("Stack has less than 2 elements");
288+
}
289+
stack.push(second);
290+
continue;
291+
}
292+
293+
// Duplicate top of stack
294+
if (nextChar === ":") {
295+
const top = pop(stack);
296+
stack.push(top);
297+
stack.push(top);
298+
continue;
299+
}
300+
301+
// Swap top two
302+
if (nextChar === "~") {
303+
const a = pop(stack);
304+
const b = pop(stack);
305+
stack.push(a);
306+
stack.push(b);
307+
continue;
308+
}
309+
310+
// Equals (true == DOWN, false == NO-OP)
311+
if (nextChar === "=") {
312+
const a = pop(stack);
313+
const b = pop(stack);
314+
if (a === b) {
315+
cursor.dir = DOWN;
316+
}
317+
continue;
318+
}
319+
320+
// Pop two and add as number
321+
if (nextChar === "+") {
322+
const a = pop(stack);
323+
const b = pop(stack);
324+
if (typeof a !== "number" || typeof b !== "number") {
325+
throw new Error(`Invalid number: ${a} ${b}`);
326+
}
327+
stack.push(a + b);
328+
continue;
329+
}
330+
331+
// Pop two and subtract as number
332+
if (nextChar === "-") {
333+
const a = pop(stack);
334+
const b = pop(stack);
335+
if (typeof a !== "number" || typeof b !== "number") {
336+
throw new Error(`Invalid number: ${a} ${b}`);
337+
}
338+
stack.push(b - a);
339+
continue;
340+
}
341+
342+
// Pop two and join as string
343+
if (nextChar === "&") {
344+
stack.push([pop(stack), pop(stack)].join(""));
345+
continue;
346+
}
347+
348+
// Pop and print
349+
if (nextChar === "!") {
350+
stdout.push(pop(stack));
351+
continue;
352+
}
353+
354+
// Exit
355+
if (nextChar === "±") {
356+
exit = true;
357+
console.log(stdout);
358+
continue;
359+
}
360+
361+
// Push to stack
362+
if (nextChar.match(/[0-9]/)) {
363+
stack.push(Number(nextChar));
364+
} else {
365+
stack.push(nextChar);
366+
}
367+
}
368+
}
369+
370+
function findOtherLadder(grid: string[][], cursor: Cursor): Cursor {
371+
const ladders = grid.flatMap((row, x) =>
372+
row.map((ch, y) => ({ x, y, ch }))
373+
).filter(cell => cell.ch === "‖");
374+
const otherLadder = ladders.find(l => !(l.x === cursor.x && l.y === cursor.y));
375+
if (!otherLadder) {
376+
throw new Error("No other ladder found");
377+
}
378+
return { x: otherLadder.x, y: otherLadder.y, dir: cursor.dir };
379+
}

0 commit comments

Comments
 (0)