feat(tutorial): Tutorial model + TutorialLoader + tutorial.yaml content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
package thb.jeanluc.adventure.loader;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.TutorialDto;
|
||||||
|
import thb.jeanluc.adventure.loader.dto.TutorialStepDto;
|
||||||
|
import thb.jeanluc.adventure.model.Tutorial;
|
||||||
|
import thb.jeanluc.adventure.model.TutorialStep;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the optional onboarding tutorial from {@code <base>/tutorial.yaml}.
|
||||||
|
* Kept separate from {@link WorldLoader}: the tutorial is onboarding, not world
|
||||||
|
* content. Returns {@link Tutorial#none()} when the file is absent.
|
||||||
|
*/
|
||||||
|
public class TutorialLoader {
|
||||||
|
|
||||||
|
/** Default classpath base directory. */
|
||||||
|
public static final String DEFAULT_BASE = "/world";
|
||||||
|
|
||||||
|
private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());
|
||||||
|
private final String basePath;
|
||||||
|
|
||||||
|
public TutorialLoader() {
|
||||||
|
this(DEFAULT_BASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TutorialLoader(String basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads the tutorial; absent file → {@link Tutorial#none()}. */
|
||||||
|
public Tutorial load() {
|
||||||
|
String resource = basePath + "/tutorial.yaml";
|
||||||
|
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||||
|
if (in == null) {
|
||||||
|
return Tutorial.none();
|
||||||
|
}
|
||||||
|
TutorialDto dto = yaml.readValue(in, TutorialDto.class);
|
||||||
|
if (dto == null) {
|
||||||
|
return Tutorial.none();
|
||||||
|
}
|
||||||
|
List<TutorialStep> steps = new ArrayList<>();
|
||||||
|
if (dto.steps() != null) {
|
||||||
|
for (TutorialStepDto s : dto.steps()) {
|
||||||
|
steps.add(new TutorialStep(
|
||||||
|
s.instruction(),
|
||||||
|
s.expect(),
|
||||||
|
s.minArgs() == null ? 0 : s.minArgs(),
|
||||||
|
s.confirm(),
|
||||||
|
s.hint()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Tutorial(dto.intro(), steps, dto.closingTips());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new WorldLoadException("Failed to parse " + resource, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package thb.jeanluc.adventure.loader.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** YAML representation of the tutorial document. */
|
||||||
|
public record TutorialDto(String intro, List<TutorialStepDto> steps, String closingTips) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package thb.jeanluc.adventure.loader.dto;
|
||||||
|
|
||||||
|
/** YAML representation of a tutorial step. */
|
||||||
|
public record TutorialStepDto(
|
||||||
|
String instruction,
|
||||||
|
String expect,
|
||||||
|
Integer minArgs,
|
||||||
|
String confirm,
|
||||||
|
String hint
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Onboarding walkthrough: an intro, ordered steps, and closing tips. */
|
||||||
|
public record Tutorial(String intro, List<TutorialStep> steps, String closingTips) {
|
||||||
|
|
||||||
|
public Tutorial {
|
||||||
|
steps = steps == null ? List.of() : List.copyOf(steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return steps.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An absent tutorial (no steps). */
|
||||||
|
public static Tutorial none() {
|
||||||
|
return new Tutorial(null, List.of(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package thb.jeanluc.adventure.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One interactive tutorial step: an instruction, the command that satisfies it
|
||||||
|
* ({@code expect} is a verb name like "look", or "go_direction"/"go_to_room"),
|
||||||
|
* a minimum argument count, a success confirmation, and a hint for a valid but
|
||||||
|
* wrong command.
|
||||||
|
*/
|
||||||
|
public record TutorialStep(
|
||||||
|
String instruction,
|
||||||
|
String expect,
|
||||||
|
int minArgs,
|
||||||
|
String confirm,
|
||||||
|
String hint
|
||||||
|
) {
|
||||||
|
}
|
||||||
40
Semesterprojekt/src/main/resources/world/tutorial.yaml
Normal file
40
Semesterprojekt/src/main/resources/world/tutorial.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
intro: |
|
||||||
|
Welcome to the manor. Let me show you the ropes — just type the commands as
|
||||||
|
I describe them. (Type 'skip' at any time to jump straight into the game.)
|
||||||
|
steps:
|
||||||
|
- instruction: "First, look around the room: type 'look'."
|
||||||
|
expect: look
|
||||||
|
confirm: "Good — 'look' (or 'l') re-describes wherever you are."
|
||||||
|
hint: "When you're ready, just type: look"
|
||||||
|
- instruction: "Examine something you noticed: 'examine <thing>' (or 'x <thing>')."
|
||||||
|
expect: examine
|
||||||
|
minArgs: 1
|
||||||
|
confirm: "That's how you inspect items, exits, and people up close."
|
||||||
|
hint: "Try examining something, e.g.: examine lamp"
|
||||||
|
- instruction: "Pick something up: 'take <item>'."
|
||||||
|
expect: take
|
||||||
|
minArgs: 1
|
||||||
|
confirm: "Taken. Things you carry go into your inventory."
|
||||||
|
hint: "Pick something up, e.g.: take lamp"
|
||||||
|
- instruction: "Check what you're carrying: 'inventory' (or 'i')."
|
||||||
|
expect: inventory
|
||||||
|
confirm: "That's your inventory."
|
||||||
|
hint: "Type: inventory"
|
||||||
|
- instruction: "Use an item: 'use <item>' — try lighting the lamp."
|
||||||
|
expect: use
|
||||||
|
minArgs: 1
|
||||||
|
confirm: "Some items toggle, read, or react when you use them."
|
||||||
|
hint: "Use something, e.g.: use lamp"
|
||||||
|
- instruction: "Now move to another room: 'go <direction>' (try 'go north')."
|
||||||
|
expect: go_direction
|
||||||
|
confirm: "You can move north / south / east / west between connected rooms."
|
||||||
|
hint: "Head to a connected room, e.g.: go north"
|
||||||
|
- instruction: "Travel back to a room you've already seen: 'go to <room>'."
|
||||||
|
expect: go_to_room
|
||||||
|
confirm: "Nice — 'go to <room>' auto-walks to any room you've already visited."
|
||||||
|
hint: "Try: go to <a room you've already visited>"
|
||||||
|
closingTips: |
|
||||||
|
That's the core. A few more you'll want:
|
||||||
|
read · drop · talk to <npc> · give <item> to <npc> ·
|
||||||
|
use <item> on <item> (combine things) · map · quests · save · menu.
|
||||||
|
Type 'help' anytime. Good luck.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package thb.jeanluc.adventure.loader;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import thb.jeanluc.adventure.model.Tutorial;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class TutorialLoaderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadsTheTestFixture() {
|
||||||
|
Tutorial t = new TutorialLoader().load(); // test-classpath /world/tutorial.yaml shadows prod
|
||||||
|
assertThat(t.isEmpty()).isFalse();
|
||||||
|
assertThat(t.intro()).isNotBlank();
|
||||||
|
assertThat(t.closingTips()).isNotBlank();
|
||||||
|
assertThat(t.steps()).hasSize(2);
|
||||||
|
assertThat(t.steps().getFirst().expect()).isEqualTo("look");
|
||||||
|
assertThat(t.steps().getFirst().minArgs()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingFileReturnsEmptyTutorial() {
|
||||||
|
Tutorial t = new TutorialLoader("/no-such-base").load();
|
||||||
|
assertThat(t.isEmpty()).isTrue();
|
||||||
|
assertThat(t.steps()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Semesterprojekt/src/test/resources/world/tutorial.yaml
Normal file
14
Semesterprojekt/src/test/resources/world/tutorial.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
intro: |
|
||||||
|
Test intro.
|
||||||
|
steps:
|
||||||
|
- instruction: "Look: type 'look'."
|
||||||
|
expect: look
|
||||||
|
confirm: "Looked."
|
||||||
|
hint: "Type look."
|
||||||
|
- instruction: "Take: type 'take <item>'."
|
||||||
|
expect: take
|
||||||
|
minArgs: 1
|
||||||
|
confirm: "Took it."
|
||||||
|
hint: "Take something."
|
||||||
|
closingTips: |
|
||||||
|
Test tips.
|
||||||
Reference in New Issue
Block a user