Skip to content

Commit 36d8712

Browse files
committed
feat: added fun example
1 parent f2f77c7 commit 36d8712

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

examples/FunShootingGallery.java

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

0 commit comments

Comments
 (0)