Skip to content

Commit 59e2997

Browse files
committed
feat: added fun example
1 parent 459236f commit 59e2997

1 file changed

Lines changed: 364 additions & 0 deletions

File tree

examples/FunShootingGallery.java

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//DEPS org.codejive.miniterm:miniterm${miniterm.ffm:}:${miniterm.version:0.1.5}
3+
//DEPS org.codejive.miniterm:ansiparser:${miniterm.version:0.1.5}
4+
//DEPS org.codejive.miniterm:mousetrack:${miniterm.version:0.1.5}
5+
6+
package examples;
7+
8+
import java.io.IOException;
9+
import java.io.UncheckedIOException;
10+
import java.util.*;
11+
import java.util.function.BiFunction;
12+
import org.codejive.miniterm.Terminal;
13+
import org.codejive.miniterm.ansiparser.AnsiReader;
14+
import org.codejive.miniterm.mousetrack.MouseEvent;
15+
import org.codejive.miniterm.mousetrack.MouseTracking;
16+
17+
public class FunShootingGallery {
18+
19+
// ── ANSI constants ────────────────────────────────────────────────────────
20+
private static final String ESC = "\033";
21+
private static final String CSI = ESC + "[";
22+
private static final String RESET = CSI + "0m";
23+
24+
// ── Target definition ─────────────────────────────────────────────────────
25+
26+
// Background colours for target layers (foreground stays default/white)
27+
private static final String CY = CSI + "106m"; // bright cyan bg — outer ring (10 pts each)
28+
private static final String YL = CSI + "103m"; // bright yellow bg — middle ring (25 pts each)
29+
private static final String RD = CSI + "1;101m"; // bold bright red bg — bullseye (50 pts)
30+
31+
/**
32+
* Visual lines of the target (5 visible chars wide × 5 lines tall).
33+
* ANSI colour codes are embedded; only the visible characters count for positioning.
34+
*
35+
* ...
36+
* .###.
37+
* .#*#.
38+
* .###.
39+
* ...
40+
*/
41+
private static final String[] TARGET_VISUAL = {
42+
" " + CY + " " + RESET,
43+
CY + " " + RD + " " + CY + " " + RESET,
44+
CY + " " + RD + " " + YL + " " + RESET + RD + " " + CY + " " + RESET,
45+
CY + " " + RD + " " + CY + " " + RESET,
46+
" " + CY + " " + RESET
47+
};
48+
49+
/**
50+
* Hit-box for the target — same visible dimensions as TARGET_VISUAL.
51+
* ' ' = miss (0 pts)
52+
* '.' = 10 pts
53+
* '#' = 25 pts
54+
* '*' = 50 pts
55+
*/
56+
private static final String[] TARGET_HITBOX = {
57+
" ... ",
58+
".###.",
59+
".#*#.",
60+
".###.",
61+
" ... "
62+
};
63+
64+
private static final String[] PLAY_BUTTON_VISUAL = {
65+
"╔════════════╗",
66+
"║ Play Again ║",
67+
"╚════════════╝"
68+
};
69+
70+
private static final String[] EXIT_BUTTON_VISUAL = {
71+
"╔════════════╗",
72+
"║ Exit Game ║",
73+
"╚════════════╝"
74+
};
75+
76+
// ── Game constants ────────────────────────────────────────────────────────
77+
private static final int MAX_TARGETS = 5;
78+
private static final long MIN_LIFE_MS = 1_000;
79+
private static final long MAX_LIFE_MS = 3_000;
80+
private static final long SPAWN_EVERY_MS = 800;
81+
82+
// ── Game state ────────────────────────────────────────────────────────────
83+
private static final class ActiveTarget {
84+
final int x, y;
85+
final int width, height;
86+
final String[] visual;
87+
final long expiresAt;
88+
final BiFunction<Integer, Integer, Boolean> onHit;
89+
ActiveTarget(int x, int y, int width, int height, String[] visual, long expiresAt,
90+
BiFunction<Integer, Integer, Boolean> onHit) {
91+
this.x = x;
92+
this.y = y;
93+
this.width = width;
94+
this.height = height;
95+
this.visual = visual;
96+
this.expiresAt = expiresAt;
97+
this.onHit = onHit;
98+
}
99+
}
100+
101+
private static final int MAX_MISSES = 10;
102+
103+
private static Terminal terminal;
104+
private static int cols, rows;
105+
private static final List<ActiveTarget> targets = new ArrayList<>();
106+
private static int score = 0;
107+
private static int misses = 0;
108+
private static int hits = 0;
109+
private static long lastSpawn = 0;
110+
private static boolean gameOver = false;
111+
private static boolean restart = false;
112+
private static boolean breakLoop = false;
113+
private static final Random rng = new Random();
114+
115+
// Speed scales up with each hit: targets live shorter, spawn faster
116+
private static long spawnInterval() { return Math.max(200, SPAWN_EVERY_MS - hits * 20L); }
117+
private static long minLife() { return Math.max(400, MIN_LIFE_MS - hits * 20L); }
118+
private static long maxLife() { return Math.max(800, MAX_LIFE_MS - hits * 40L); }
119+
120+
// ── Entry point ───────────────────────────────────────────────────────────
121+
public static void main(String[] args) throws Exception {
122+
try (Terminal term = Terminal.create()) {
123+
terminal = term;
124+
var sz = terminal.size();
125+
cols = sz.width;
126+
rows = sz.height;
127+
128+
terminal.enableRawMode();
129+
terminal.onResize(s -> { cols = s.width; rows = s.height; });
130+
131+
// Button-click mouse tracking with SGR extended coordinates
132+
MouseTracking.enable(terminal, MouseTracking.Protocol.BUTTON_MOTION);
133+
MouseTracking.enableEncoding(terminal, MouseTracking.Encoding.SGR);
134+
135+
// Enter alternate screen, hide cursor
136+
terminal.write(CSI + "?1049h" + CSI + "?25l");
137+
138+
try {
139+
do {
140+
// Reset state for (re)start
141+
gameOver = false; restart = false; breakLoop = false;
142+
score = 0; misses = 0; hits = 0; lastSpawn = 0;
143+
targets.clear();
144+
terminal.write(CSI + "2J");
145+
drawScore();
146+
147+
// terminal.read(50) returns -2 on timeout and -1 on EOF.
148+
// AnsiReader maps -2 (timeout) → "" and -1 (EOF) → null.
149+
AnsiReader reader = new AnsiReader(() -> terminal.read(50));
150+
String token;
151+
while ((token = reader.read()) != null) {
152+
long now = System.currentTimeMillis();
153+
154+
// ── Handle input ─────────────────────────────────────────
155+
if (!token.isEmpty()) {
156+
if (!token.startsWith(ESC) && token.charAt(0) == 3) break; // Ctrl+C
157+
if (MouseTracking.isMouseEvent(token)) {
158+
MouseEvent ev = MouseTracking.parse(token);
159+
if (ev.type() == MouseEvent.Type.PRESS) {
160+
handleClick(ev);
161+
if (breakLoop) break;
162+
}
163+
}
164+
}
165+
166+
if (!gameOver) {
167+
// ── Expire old targets ────────────────────────────────────
168+
Iterator<ActiveTarget> it = targets.iterator();
169+
while (it.hasNext()) {
170+
ActiveTarget t = it.next();
171+
if (now >= t.expiresAt) {
172+
eraseTarget(t);
173+
it.remove();
174+
score -= 5;
175+
drawScore();
176+
}
177+
}
178+
179+
// ── Spawn a new target every spawnInterval() ──────────────
180+
if (now - lastSpawn >= spawnInterval()) {
181+
lastSpawn = now;
182+
if (targets.size() < MAX_TARGETS) {
183+
spawnTarget();
184+
}
185+
}
186+
}
187+
}
188+
} while (restart);
189+
} finally {
190+
MouseTracking.disableEncoding(terminal, MouseTracking.Encoding.SGR);
191+
MouseTracking.disable(terminal, MouseTracking.Protocol.BUTTON_MOTION);
192+
// Exit alternate screen, restore cursor
193+
terminal.write(CSI + "?1049l" + CSI + "?25h");
194+
}
195+
}
196+
System.out.println("Final score: " + score);
197+
}
198+
199+
// ── Game logic ────────────────────────────────────────────────────────────
200+
private static void handleClick(MouseEvent ev) throws IOException {
201+
int cx = ev.x();
202+
int cy = ev.y();
203+
Iterator<ActiveTarget> it = targets.iterator();
204+
while (it.hasNext()) {
205+
ActiveTarget t = it.next();
206+
int lx = cx - t.x; // column within target (0-based)
207+
int ly = cy - t.y; // row within target (0-based)
208+
if (lx >= 0 && lx < t.width && ly >= 0 && ly < t.height) {
209+
if (t.onHit.apply(lx, ly)) {
210+
eraseTarget(t);
211+
it.remove();
212+
return;
213+
}
214+
}
215+
}
216+
// No target was hit — count as miss
217+
if (!gameOver) {
218+
misses++;
219+
drawScore();
220+
if (misses >= MAX_MISSES) {
221+
gameOver = true;
222+
drawGameOver();
223+
}
224+
}
225+
}
226+
227+
private static int scoreForChar(char c) {
228+
if (c == '.') return 10;
229+
if (c == '#') return 25;
230+
if (c == '*') return 50;
231+
return 0; // space = miss
232+
}
233+
234+
private static void spawnTarget() throws IOException {
235+
int width = TARGET_HITBOX[0].length();
236+
int height = TARGET_HITBOX.length;
237+
int minX = 1, maxX = cols - width;
238+
int minY = 3, maxY = rows - height; // rows 1-2 are the status bar
239+
if (maxX < minX || maxY < minY) return; // terminal too small
240+
241+
for (int attempt = 0; attempt < 20; attempt++) {
242+
int nx = minX + rng.nextInt(maxX - minX + 1);
243+
int ny = minY + rng.nextInt(maxY - minY + 1);
244+
245+
boolean overlaps = false;
246+
for (ActiveTarget t : targets) {
247+
// Add a 1-cell gap so targets are visually separated
248+
if (nx < t.x + t.width + 1 && nx + width + 1 > t.x
249+
&& ny < t.y + t.height + 1 && ny + height + 1 > t.y) {
250+
overlaps = true;
251+
break;
252+
}
253+
}
254+
if (!overlaps) {
255+
long life = minLife() + rng.nextInt((int)(maxLife() - minLife()) + 1);
256+
ActiveTarget t = new ActiveTarget(nx, ny, width, height, TARGET_VISUAL, System.currentTimeMillis() + life,
257+
(lx, ly) -> {
258+
int pts = scoreForChar(TARGET_HITBOX[ly].charAt(lx));
259+
if (pts > 0) {
260+
score += pts;
261+
hits++;
262+
drawScore();
263+
return true;
264+
}
265+
return false;
266+
});
267+
targets.add(t);
268+
drawTarget(t);
269+
return;
270+
}
271+
}
272+
}
273+
274+
// ── Rendering ─────────────────────────────────────────────────────────────
275+
private static void drawTarget(ActiveTarget t) throws IOException {
276+
for (int i = 0; i < t.height; i++) {
277+
terminal.write(moveTo(t.x, t.y + i) + t.visual[i]);
278+
}
279+
terminal.write(RESET);
280+
}
281+
282+
private static void eraseTarget(ActiveTarget t) throws IOException {
283+
String blank = " ".repeat(t.width);
284+
for (int i = 0; i < t.height; i++) {
285+
terminal.write(moveTo(t.x, t.y + i) + blank);
286+
}
287+
}
288+
289+
private static void drawScore() {
290+
String scoreStr = " SCORE: " + score + " ";
291+
String missStr = " MISSES: " + misses + "/" + MAX_MISSES + " ";
292+
String help = " Click targets to score! Ctrl+C to quit ";
293+
int missCol = scoreStr.length() + 1;
294+
int helpCol = Math.max(missCol + missStr.length() + 1, cols - help.length() + 1);
295+
try {
296+
terminal.write(
297+
moveTo(1, 1) + CSI + "1;33m" + scoreStr + RESET +
298+
moveTo(missCol, 1) + CSI + "1;91m" + missStr + RESET +
299+
moveTo(helpCol, 1) + CSI + "90m" + help + RESET);
300+
} catch (IOException e) {
301+
throw new UncheckedIOException(e);
302+
}
303+
}
304+
305+
private static void drawGameOver() {
306+
String[] lines = {
307+
" ██████╗ █████╗ ███╗ ███╗███████╗ ",
308+
" ██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ",
309+
" ██║ ███╗███████║██╔████╔██║█████╗ ",
310+
" ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ",
311+
" ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ",
312+
" ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ",
313+
" ",
314+
" ██████╗ ██╗ ██╗███████╗██████╗ ",
315+
" ██╔═══██╗██║ ██║██╔════╝██╔══██╗ ",
316+
" ██║ ██║██║ ██║█████╗ ██████╔╝ ",
317+
" ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ",
318+
" ╚██████╔╝ ╚████╔╝ ███████╗██║ ██║ ",
319+
" ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ",
320+
" ",
321+
" Score: " + score + " Misses: " + misses
322+
};
323+
int startRow = Math.max(1, (rows - lines.length) / 2);
324+
int maxLen = 0;
325+
for (String l : lines) maxLen = Math.max(maxLen, l.length());
326+
int startCol = Math.max(1, (cols - maxLen) / 2);
327+
try {
328+
terminal.write(CSI + "1;31m");
329+
for (int i = 0; i < lines.length; i++) {
330+
terminal.write(moveTo(startCol, startRow + i) + lines[i]);
331+
}
332+
terminal.write(RESET);
333+
334+
// Clear remaining game targets and show button choices
335+
//for (ActiveTarget t : targets) eraseTarget(t);
336+
targets.clear();
337+
338+
int btnRow = startRow + lines.length + 2;
339+
int btnWidth = PLAY_BUTTON_VISUAL[0].length();
340+
int btnHeight = PLAY_BUTTON_VISUAL.length;
341+
int gap = 4;
342+
int playCol = Math.max(1, (cols - btnWidth * 2 - gap) / 2);
343+
int exitCol = playCol + btnWidth + gap;
344+
345+
ActiveTarget playBtn = new ActiveTarget(playCol, btnRow, btnWidth, btnHeight,
346+
PLAY_BUTTON_VISUAL, Long.MAX_VALUE,
347+
(lx, ly) -> { restart = true; breakLoop = true; return true; });
348+
targets.add(playBtn);
349+
drawTarget(playBtn);
350+
351+
ActiveTarget exitBtn = new ActiveTarget(exitCol, btnRow, btnWidth, btnHeight,
352+
EXIT_BUTTON_VISUAL, Long.MAX_VALUE,
353+
(lx, ly) -> { breakLoop = true; return true; });
354+
targets.add(exitBtn);
355+
drawTarget(exitBtn);
356+
} catch (IOException e) {
357+
throw new UncheckedIOException(e);
358+
}
359+
}
360+
361+
private static String moveTo(int col, int row) {
362+
return CSI + row + ";" + col + "H";
363+
}
364+
}

0 commit comments

Comments
 (0)