Skip to content

Commit f0811ac

Browse files
committed
Snake game
1 parent 466ad91 commit f0811ac

File tree

9 files changed

+471
-20
lines changed

9 files changed

+471
-20
lines changed

src/FileSystem.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const files: FileSystem = {
2424
type: "-",
2525
permissions: "rwx------",
2626
contents: `
27-
Hello World!
27+
Hello World! 👋
2828
`
2929
}
3030
}
@@ -40,7 +40,28 @@ const files: FileSystem = {
4040
Looking for something custom made? I might be able to bring your vision to life.
4141
Contact me using the links under my profile picture to get started.
4242
`
43-
}
43+
},
44+
...(date.month === 3 &&
45+
date.day === 14 && {
46+
"pi.txt": {
47+
type: "-",
48+
permissions: "rwx------",
49+
contents: `Happy Pi Day! 🥧
50+
51+
Here are the first 100 digits of π:
52+
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679
53+
54+
Fun fact: π is good to eat!`
55+
}
56+
}),
57+
...(date.month === 1 &&
58+
date.day === 1 && {
59+
"newyear.txt": {
60+
type: "-",
61+
permissions: "rwx------",
62+
contents: `🎉 Happy New Year ${date.year}! 🎊`
63+
}
64+
}),
4465
}
4566
},
4667
Music: {
@@ -59,22 +80,22 @@ const files: FileSystem = {
5980
},
6081
...(date.month === 5 &&
6182
date.day === 16 && {
62-
"itsMyBirthday.mkv": {
63-
type: "-",
64-
permissions: "rwx------",
65-
contents:
66-
"https://www.youtube.com/embed/nYsbt8Fo9Ow?controls=0&controls=0autoplay=1"
67-
}
68-
}),
83+
"itsMyBirthday.mkv": {
84+
type: "-",
85+
permissions: "rwx------",
86+
contents:
87+
"https://www.youtube.com/embed/nYsbt8Fo9Ow?controls=0&controls=0autoplay=1"
88+
}
89+
}),
6990
...(date.month === 9 &&
7091
date.day === 21 && {
71-
"isItThatTime.avi": {
72-
type: "-",
73-
permissions: "rwx------",
74-
contents:
75-
"https://www.youtube.com/embed/Gs069dndIYk?controls=0&controls=0autoplay=1"
76-
}
77-
})
92+
"isItThatTime.avi": {
93+
type: "-",
94+
permissions: "rwx------",
95+
contents:
96+
"https://www.youtube.com/embed/Gs069dndIYk?controls=0&controls=0autoplay=1"
97+
}
98+
})
7899
}
79100
},
80101
Videos: {

src/components/terminal/Terminal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ const Terminal = (
207207
`;
208208

209209
const passwordPrompt = emulatorState.getPasswordPromptState();
210+
210211
return (
211212
<>
212213
{showMOTD && (
@@ -261,6 +262,9 @@ const Terminal = (
261262
<AdminConsole emulatorState={emulatorState} theme={props.theme} />
262263
</Box>
263264
)}
265+
{emulatorState.getBlockingMode()?.content && (
266+
<Box>{emulatorState.getBlockingMode()?.content}</Box>
267+
)}
264268
{passwordPrompt ? (
265269
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
266270
<Box component="span" style={{ color: props.theme.outputColor }}>
@@ -318,7 +322,7 @@ const Terminal = (
318322
}}
319323
/>
320324
</Grid>
321-
) : (
325+
) : emulatorState.getBlockingMode() ? null : (
322326
<CommandInput
323327
ref={ref}
324328
value={input}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { useEffect, useState, useRef } from "react";
2+
import { Box, Typography } from "@mui/material";
3+
import EmulatorState from "../../../javascript-terminal/emulator-state/EmulatorState";
4+
5+
type Position = { x: number; y: number };
6+
7+
const GRID_WIDTH = 25;
8+
const GRID_HEIGHT = 12;
9+
const CELL_SIZE = 20;
10+
const INITIAL_SNAKE: Position[] = [{ x: 12, y: 6 }];
11+
const INITIAL_DIRECTION: Position = { x: 1, y: 0 };
12+
const GAME_SPEED = 150;
13+
14+
const SnakeGame = ({ emulatorState }: { emulatorState: EmulatorState }) => {
15+
const [snake, setSnake] = useState<Position[]>(INITIAL_SNAKE);
16+
const [direction, setDirection] = useState<Position>(INITIAL_DIRECTION);
17+
const [food, setFood] = useState<Position>({ x: 18, y: 6 });
18+
const [gameOver, setGameOver] = useState(false);
19+
const [score, setScore] = useState(0);
20+
const [countdown, setCountdown] = useState(3);
21+
const [loading, setLoading] = useState(true);
22+
const directionRef = useRef(direction);
23+
24+
const generateFood = (): Position => {
25+
let newFood: Position;
26+
do {
27+
newFood = {
28+
x: Math.floor(Math.random() * GRID_WIDTH),
29+
y: Math.floor(Math.random() * GRID_HEIGHT)
30+
};
31+
} while (snake.some((segment) => segment.x === newFood.x && segment.y === newFood.y));
32+
return newFood;
33+
};
34+
35+
useEffect(() => {
36+
directionRef.current = direction;
37+
}, [direction]);
38+
39+
useEffect(() => {
40+
if (gameOver) {
41+
const timer = setTimeout(() => {
42+
emulatorState.setBlockingMode(undefined);
43+
}, 1500);
44+
return () => clearTimeout(timer);
45+
}
46+
}, [gameOver, emulatorState]);
47+
48+
useEffect(() => {
49+
if (countdown > 0) {
50+
const timer = setTimeout(() => {
51+
setCountdown(countdown - 1);
52+
}, 1000);
53+
return () => clearTimeout(timer);
54+
} else {
55+
setLoading(false);
56+
}
57+
}, [countdown]);
58+
59+
useEffect(() => {
60+
const handleKeyPress = (e: KeyboardEvent) => {
61+
if (gameOver || loading) return;
62+
63+
const key = e.key;
64+
const currentDir = directionRef.current;
65+
66+
if (key === "ArrowUp" && currentDir.y === 0) {
67+
e.preventDefault();
68+
setDirection({ x: 0, y: -1 });
69+
} else if (key === "ArrowDown" && currentDir.y === 0) {
70+
e.preventDefault();
71+
setDirection({ x: 0, y: 1 });
72+
} else if (key === "ArrowLeft" && currentDir.x === 0) {
73+
e.preventDefault();
74+
setDirection({ x: -1, y: 0 });
75+
} else if (key === "ArrowRight" && currentDir.x === 0) {
76+
e.preventDefault();
77+
setDirection({ x: 1, y: 0 });
78+
}
79+
};
80+
81+
window.addEventListener("keydown", handleKeyPress);
82+
return () => window.removeEventListener("keydown", handleKeyPress);
83+
}, [gameOver, loading]);
84+
85+
useEffect(() => {
86+
if (gameOver || loading) return;
87+
88+
const gameLoop = setInterval(() => {
89+
setSnake((prevSnake) => {
90+
const head = prevSnake[0];
91+
const newHead: Position = {
92+
x: head.x + directionRef.current.x,
93+
y: head.y + directionRef.current.y
94+
};
95+
96+
if (
97+
newHead.x < 0 ||
98+
newHead.x >= GRID_WIDTH ||
99+
newHead.y < 0 ||
100+
newHead.y >= GRID_HEIGHT
101+
) {
102+
setGameOver(true);
103+
return prevSnake;
104+
}
105+
106+
if (
107+
prevSnake.some((segment) => segment.x === newHead.x && segment.y === newHead.y)
108+
) {
109+
setGameOver(true);
110+
return prevSnake;
111+
}
112+
113+
const newSnake = [newHead, ...prevSnake];
114+
115+
if (newHead.x === food.x && newHead.y === food.y) {
116+
setScore((prev) => prev + 10);
117+
setFood(generateFood());
118+
return newSnake;
119+
}
120+
121+
newSnake.pop();
122+
return newSnake;
123+
});
124+
}, GAME_SPEED);
125+
126+
return () => clearInterval(gameLoop);
127+
}, [gameOver, food, loading]);
128+
129+
return (
130+
<Box sx={{ padding: 2 }}>
131+
<Typography sx={{ color: "#2BC903", mb: 1, fontFamily: "monospace" }}>
132+
SNAKE - Score: {score}
133+
</Typography>
134+
{loading ? (
135+
<Box
136+
sx={{
137+
display: "flex",
138+
alignItems: "center",
139+
justifyContent: "center",
140+
width: GRID_WIDTH * CELL_SIZE + 4,
141+
height: GRID_HEIGHT * CELL_SIZE + 4,
142+
border: "2px solid #2BC903"
143+
}}
144+
>
145+
<Typography sx={{ color: "#2BC903", fontSize: "1.5em" }}>
146+
{countdown > 0 ? countdown : "GO!"}
147+
</Typography>
148+
</Box>
149+
) : gameOver ? (
150+
<Box
151+
sx={{
152+
display: "flex",
153+
flexDirection: "column",
154+
alignItems: "center",
155+
justifyContent: "center",
156+
width: GRID_WIDTH * CELL_SIZE + 4,
157+
height: GRID_HEIGHT * CELL_SIZE + 4,
158+
border: "2px solid #ff0606",
159+
gap: 2
160+
}}
161+
>
162+
<Typography sx={{ color: "#ff0606", fontSize: "2em", fontWeight: "bold" }}>
163+
GAME OVER
164+
</Typography>
165+
<Typography sx={{ color: "#FCFCFC", fontSize: "1.2em" }}>
166+
Final Score: {score}
167+
</Typography>
168+
</Box>
169+
) : (
170+
<Box
171+
sx={{
172+
display: "grid",
173+
gridTemplateColumns: `repeat(${GRID_WIDTH}, ${CELL_SIZE}px)`,
174+
gridTemplateRows: `repeat(${GRID_HEIGHT}, ${CELL_SIZE}px)`,
175+
gap: 0,
176+
border: "2px solid #2BC903",
177+
width: "fit-content"
178+
}}
179+
>
180+
{Array.from({ length: GRID_WIDTH * GRID_HEIGHT }).map((_, index) => {
181+
const x = index % GRID_WIDTH;
182+
const y = Math.floor(index / GRID_WIDTH);
183+
const snakeIndex = snake.findIndex(
184+
(segment) => segment.x === x && segment.y === y
185+
);
186+
const isHead = snakeIndex === 0;
187+
const isSnake = snakeIndex !== -1;
188+
const isFood = food.x === x && food.y === y;
189+
190+
return (
191+
<Box
192+
key={index}
193+
sx={{
194+
width: CELL_SIZE,
195+
height: CELL_SIZE,
196+
backgroundColor: isSnake
197+
? "#2BC903"
198+
: isFood
199+
? "#ff0606"
200+
: "#121212",
201+
border: "1px solid #1a1a1a",
202+
display: "flex",
203+
alignItems: "center",
204+
justifyContent: "center",
205+
fontSize: "10px",
206+
color: "#000000",
207+
fontWeight: "bold"
208+
}}
209+
>
210+
{isHead && "• •"}
211+
</Box>
212+
);
213+
})}
214+
</Box>
215+
)}
216+
<Typography sx={{ color: "#FCFCFC", mt: 1, fontSize: "0.9em" }}>
217+
Use arrow keys to move
218+
</Typography>
219+
</Box>
220+
);
221+
};
222+
223+
export default SnakeGame;

src/components/terminal/output/OutputText.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TerminalTheme } from "../Terminal";
44
const OutputText = (props: { theme: TerminalTheme; children: string }) => {
55
const text = props.children ?? "";
66
return text.split("\n").map((line: string, key: number) => (
7-
<Typography key={key} style={{ color: props.theme.outputColor }}>
7+
<Typography key={key} style={{ color: props.theme.outputColor, whiteSpace: "pre" }}>
88
{line}
99
</Typography>
1010
));

0 commit comments

Comments
 (0)