feat(io): GameIO.choose numbered-menu primitive (console default)
This commit is contained in:
@@ -9,6 +9,8 @@ import thb.jeanluc.adventure.io.text.Renderings;
|
|||||||
import thb.jeanluc.adventure.io.text.RoomView;
|
import thb.jeanluc.adventure.io.text.RoomView;
|
||||||
import thb.jeanluc.adventure.io.text.StyledText;
|
import thb.jeanluc.adventure.io.text.StyledText;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bidirectional channel between the engine and the player. Commands emit
|
* Bidirectional channel between the engine and the player. Commands emit
|
||||||
* semantic, role-styled output via {@link #print(StyledText)} (the primitive)
|
* semantic, role-styled output via {@link #print(StyledText)} (the primitive)
|
||||||
@@ -56,4 +58,38 @@ public interface GameIO {
|
|||||||
default void showQuests(QuestView view) {
|
default void showQuests(QuestView view) {
|
||||||
print(QuestText.render(view));
|
print(QuestText.render(view));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presents a titled list of options and returns the 0-based index of the
|
||||||
|
* player's choice. Default (console) prints a numbered list and reads a
|
||||||
|
* number, re-prompting on invalid input. EOF / empty input returns the
|
||||||
|
* LAST option (menus place the safe/cancel option last). Frontends with
|
||||||
|
* native widgets (e.g. buttons) may override.
|
||||||
|
*
|
||||||
|
* @param title heading shown above the options
|
||||||
|
* @param options non-empty list of option labels
|
||||||
|
* @return 0-based index into {@code options}
|
||||||
|
*/
|
||||||
|
default int choose(String title, List<String> options) {
|
||||||
|
StyledText.Builder b = StyledText.builder().heading(title).plain("\n");
|
||||||
|
for (int i = 0; i < options.size(); i++) {
|
||||||
|
b.plain(" " + (i + 1) + ") " + options.get(i) + "\n");
|
||||||
|
}
|
||||||
|
print(b.build());
|
||||||
|
while (true) {
|
||||||
|
String line = readLine();
|
||||||
|
if (line == null || line.isBlank()) {
|
||||||
|
return options.size() - 1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int n = Integer.parseInt(line.trim());
|
||||||
|
if (n >= 1 && n <= options.size()) {
|
||||||
|
return n - 1;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// fall through to re-prompt
|
||||||
|
}
|
||||||
|
write("Please enter a number between 1 and " + options.size() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package thb.jeanluc.adventure.io;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class ChooseDefaultTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsZeroBasedIndexOfValidChoice() {
|
||||||
|
TestIO io = new TestIO().enqueue("2");
|
||||||
|
int i = io.choose("Menu", List.of("A", "B", "C"));
|
||||||
|
assertThat(i).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rePromptsOnInvalidThenAccepts() {
|
||||||
|
TestIO io = new TestIO().enqueue("9").enqueue("x").enqueue("1");
|
||||||
|
int i = io.choose("Menu", List.of("A", "B"));
|
||||||
|
assertThat(i).isZero();
|
||||||
|
assertThat(io.allOutput()).contains("between 1 and 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void eofReturnsLastOption() {
|
||||||
|
TestIO io = new TestIO(); // empty queue → readLine() returns null
|
||||||
|
int i = io.choose("Menu", List.of("Play", "Quit"));
|
||||||
|
assertThat(i).isEqualTo(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user