feat(io): ConsoleIO ANSI + box-drawing renderer with colour/glyph modes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,43 +1,57 @@
|
||||
package thb.jeanluc.adventure.io;
|
||||
|
||||
import thb.jeanluc.adventure.io.text.Hud;
|
||||
import thb.jeanluc.adventure.io.text.RoomView;
|
||||
import thb.jeanluc.adventure.io.text.Span;
|
||||
import thb.jeanluc.adventure.io.text.Style;
|
||||
import thb.jeanluc.adventure.io.text.StyledText;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import thb.jeanluc.adventure.io.text.StyledText;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link GameIO} backed by {@code System.in} and {@code System.out}.
|
||||
* Each {@code write} appends a newline so output blocks are visually
|
||||
* separated. {@code readLine} prints a {@code > } prompt before
|
||||
* blocking.
|
||||
* {@link GameIO} for the terminal: ANSI colours, box-drawing room frames
|
||||
* (level "B"), and a dim HUD line. Colour and glyph fidelity are configurable
|
||||
* and degrade safely (no colours when output is piped; ASCII frames on request).
|
||||
*/
|
||||
public class ConsoleIO implements GameIO {
|
||||
|
||||
/** Input source. */
|
||||
/** Whether ANSI colour is emitted. */
|
||||
public enum ColorMode { ON, OFF, AUTO }
|
||||
|
||||
/** Frame/glyph fidelity tier. */
|
||||
public enum GlyphMode { ASCII, UNICODE, GLYPH }
|
||||
|
||||
private static final int FRAME_WIDTH = 44;
|
||||
|
||||
private final BufferedReader in;
|
||||
|
||||
/** Output sink. */
|
||||
private final PrintStream out;
|
||||
private final boolean useColor;
|
||||
private final GlyphMode glyphs;
|
||||
|
||||
/**
|
||||
* Creates a console IO using standard input and output.
|
||||
*/
|
||||
public ConsoleIO() {
|
||||
this(new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)), System.out);
|
||||
this(new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)),
|
||||
System.out, ColorMode.AUTO, GlyphMode.UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-friendly constructor allowing custom streams.
|
||||
*
|
||||
* @param in reader to consume player input
|
||||
* @param out stream to write player output
|
||||
*/
|
||||
/** Backward-compatible stream constructor (colour off, unicode frames). */
|
||||
public ConsoleIO(BufferedReader in, PrintStream out) {
|
||||
this(in, out, ColorMode.OFF, GlyphMode.UNICODE);
|
||||
}
|
||||
|
||||
public ConsoleIO(BufferedReader in, PrintStream out, ColorMode colorMode, GlyphMode glyphs) {
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
this.glyphs = glyphs;
|
||||
this.useColor = switch (colorMode) {
|
||||
case ON -> true;
|
||||
case OFF -> false;
|
||||
case AUTO -> System.console() != null;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +68,85 @@ public class ConsoleIO implements GameIO {
|
||||
|
||||
@Override
|
||||
public void print(StyledText text) {
|
||||
out.println(text.plainText());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Span s : text.spans()) {
|
||||
sb.append(paint(s.style(), s.text()));
|
||||
}
|
||||
out.println(sb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showRoom(RoomView room) {
|
||||
out.println(paint(Style.HEADING, frameTop(room.name())));
|
||||
out.println(room.description().stripTrailing());
|
||||
if (!room.items().isEmpty()) {
|
||||
out.println(section("Items", room.items(), Style.ITEM));
|
||||
}
|
||||
if (!room.npcs().isEmpty()) {
|
||||
out.println(section("People", room.npcs(), Style.NPC));
|
||||
}
|
||||
if (!room.exits().isEmpty()) {
|
||||
out.println(section("Exits", room.exits(), Style.EXIT));
|
||||
} else {
|
||||
out.println(" " + paint(Style.DIM, "no obvious exits"));
|
||||
}
|
||||
out.println(paint(Style.HEADING, frameBottom()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHud(Hud h) {
|
||||
String line = "[ " + h.location() + " · " + h.gold() + " gold · turn " + h.turn()
|
||||
+ " · light: " + (h.lightOn() ? "on" : "off") + " ]";
|
||||
out.println(paint(Style.DIM, line));
|
||||
}
|
||||
|
||||
private String section(String label, List<String> xs, Style st) {
|
||||
StringBuilder sb = new StringBuilder(" ").append(paint(Style.DIM, label)).append(" ");
|
||||
for (int i = 0; i < xs.size(); i++) {
|
||||
if (i > 0) {
|
||||
sb.append(" · ");
|
||||
}
|
||||
sb.append(paint(st, xs.get(i)));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String frameTop(String title) {
|
||||
String tl = glyphs == GlyphMode.ASCII ? "+" : "┌";
|
||||
String tr = glyphs == GlyphMode.ASCII ? "+" : "┐";
|
||||
String h = glyphs == GlyphMode.ASCII ? "-" : "─";
|
||||
StringBuilder sb = new StringBuilder(tl).append(h).append(' ')
|
||||
.append(title.toUpperCase()).append(' ');
|
||||
while (sb.length() < FRAME_WIDTH - 1) {
|
||||
sb.append(h);
|
||||
}
|
||||
return sb.append(tr).toString();
|
||||
}
|
||||
|
||||
private String frameBottom() {
|
||||
String bl = glyphs == GlyphMode.ASCII ? "+" : "└";
|
||||
String br = glyphs == GlyphMode.ASCII ? "+" : "┘";
|
||||
String h = glyphs == GlyphMode.ASCII ? "-" : "─";
|
||||
StringBuilder sb = new StringBuilder(bl);
|
||||
while (sb.length() < FRAME_WIDTH - 1) {
|
||||
sb.append(h);
|
||||
}
|
||||
return sb.append(br).toString();
|
||||
}
|
||||
|
||||
private String paint(Style st, String s) {
|
||||
if (!useColor) {
|
||||
return s;
|
||||
}
|
||||
String code = switch (st) {
|
||||
case HEADING -> "[1;35m";
|
||||
case ITEM -> "[36m";
|
||||
case NPC -> "[33m";
|
||||
case EXIT -> "[32m";
|
||||
case DANGER -> "[31m";
|
||||
case DIM -> "[90m";
|
||||
case PLAIN -> "";
|
||||
};
|
||||
return code.isEmpty() ? s : code + s + "[0m";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package thb.jeanluc.adventure.io;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import thb.jeanluc.adventure.io.text.Hud;
|
||||
import thb.jeanluc.adventure.io.text.RoomView;
|
||||
import thb.jeanluc.adventure.io.text.StyledText;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ConsoleIORenderTest {
|
||||
|
||||
private ConsoleIO io(ByteArrayOutputStream sink) {
|
||||
PrintStream ps = new PrintStream(sink, true, StandardCharsets.UTF_8);
|
||||
return new ConsoleIO(new BufferedReader(new StringReader("")), ps,
|
||||
ConsoleIO.ColorMode.OFF, ConsoleIO.GlyphMode.UNICODE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void showRoom_drawsHeadingSectionsAndExits() {
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream();
|
||||
io(sink).showRoom(new RoomView("Kitchen", "A cold kitchen.",
|
||||
List.of("Lamp"), List.of("Old Man"), List.of("north", "east")));
|
||||
String out = sink.toString(StandardCharsets.UTF_8);
|
||||
assertThat(out).contains("KITCHEN");
|
||||
assertThat(out).contains("A cold kitchen.");
|
||||
assertThat(out).contains("Lamp");
|
||||
assertThat(out).contains("Old Man");
|
||||
assertThat(out).contains("north").contains("east");
|
||||
}
|
||||
|
||||
@Test
|
||||
void print_colorOff_emitsNoAnsiEscapes() {
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream();
|
||||
io(sink).print(StyledText.builder().item("brass lamp").build());
|
||||
assertThat(sink.toString(StandardCharsets.UTF_8)).doesNotContain("[");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setHud_printsLocationGoldTurnAndLight() {
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream();
|
||||
io(sink).setHud(new Hud("Kitchen", 0, 7, false));
|
||||
String out = sink.toString(StandardCharsets.UTF_8);
|
||||
assertThat(out).contains("Kitchen").contains("turn 7").contains("light: off");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asciiGlyphMode_usesPlusDashFrame() {
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream();
|
||||
PrintStream ps = new PrintStream(sink, true, StandardCharsets.UTF_8);
|
||||
new ConsoleIO(new BufferedReader(new StringReader("")), ps,
|
||||
ConsoleIO.ColorMode.OFF, ConsoleIO.GlyphMode.ASCII)
|
||||
.showRoom(new RoomView("Cell", "x", List.of(), List.of(), List.of("north")));
|
||||
String out = sink.toString(StandardCharsets.UTF_8);
|
||||
assertThat(out).contains("+").contains("-");
|
||||
assertThat(out).doesNotContain("─");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user