feat(game): Combinations engine for use X on Y

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:22:16 +02:00
parent 667991df26
commit 89962b4b60
2 changed files with 208 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
package thb.jeanluc.adventure.game;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Combination;
import thb.jeanluc.adventure.model.Condition;
import thb.jeanluc.adventure.model.Effect;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class CombinationsTest {
private Item plain(String id, boolean light) {
return PlainItem.builder().id(id).name(id).description("d").light(light).build();
}
/** World with matches/torch/lit_torch/shovel and the given recipes; player in an empty room. */
private GameContext ctx(Combination... recipes) {
Map<String, Item> items = new LinkedHashMap<>();
items.put("matches", plain("matches", false));
items.put("torch", plain("torch", false));
items.put("lit_torch", plain("lit_torch", true));
items.put("shovel", plain("shovel", false));
Map<String, Combination> combos = new HashMap<>();
for (Combination c : recipes) {
combos.put(c.key(), c);
}
Room start = new Room("start", "Start", "d");
World w = new World(Map.of("start", start), items, Map.of(), "t", "w",
Map.of(), List.of(), combos);
return new GameContext(w, new Player(start, 0), new TestIO());
}
private void give(GameContext ctx, String id) {
ctx.getPlayer().addItem(ctx.getWorld().getItems().get(id));
}
private void putInRoom(GameContext ctx, String id) {
ctx.getPlayer().getCurrentRoom().addItem(ctx.getWorld().getItems().get(id));
}
private String out(GameContext ctx) {
return ((TestIO) ctx.getIo()).allOutput();
}
private Combination torchRecipe() {
return new Combination("matches", "torch", List.of(),
List.of("matches", "torch"), "lit_torch", List.of(),
"You light the torch.", null);
}
@Test
void combinesFromInventory() {
GameContext ctx = ctx(torchRecipe());
give(ctx, "matches");
give(ctx, "torch");
Combinations.tryUse(ctx, "matches", "torch");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
assertThat(ctx.getPlayer().hasItem("matches")).isFalse();
assertThat(ctx.getPlayer().hasItem("torch")).isFalse();
assertThat(out(ctx)).contains("light the torch");
}
@Test
void matchIsOrderIndependent() {
GameContext ctx = ctx(torchRecipe());
give(ctx, "matches");
give(ctx, "torch");
Combinations.tryUse(ctx, "torch", "matches");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
}
@Test
void consumesOperandFromCurrentRoom() {
GameContext ctx = ctx(torchRecipe());
give(ctx, "matches");
putInRoom(ctx, "torch");
Combinations.tryUse(ctx, "matches", "torch");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
assertThat(ctx.getPlayer().getCurrentRoom().findItem("torch")).isEmpty();
}
@Test
void missingOperandReportsNoSuch() {
GameContext ctx = ctx(torchRecipe());
give(ctx, "matches"); // torch absent
Combinations.tryUse(ctx, "matches", "torch");
assertThat(out(ctx)).contains("no 'torch'");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isFalse();
}
@Test
void noRecipeForPairSaysNothingHappens() {
GameContext ctx = ctx(torchRecipe());
give(ctx, "shovel");
give(ctx, "torch");
Combinations.tryUse(ctx, "shovel", "torch");
assertThat(out(ctx)).contains("Nothing happens.");
}
@Test
void unmetRequiresShowsFailTextAndDoesNotProduce() {
Combination gated = new Combination("matches", "torch",
List.of(new Condition(Condition.Type.FLAG, "dry")),
List.of("matches", "torch"), "lit_torch", List.of(),
"It catches.", "The torch is too damp to light.");
GameContext ctx = ctx(gated);
give(ctx, "matches");
give(ctx, "torch");
Combinations.tryUse(ctx, "matches", "torch");
assertThat(out(ctx)).contains("too damp");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isFalse();
assertThat(ctx.getPlayer().hasItem("matches")).isTrue(); // not consumed
}
@Test
void metRequiresAppliesEffectsAndProduces() {
Combination gated = new Combination("matches", "torch",
List.of(new Condition(Condition.Type.FLAG, "dry")),
List.of("matches", "torch"), "lit_torch",
List.of(new Effect(Effect.Type.SET_FLAG, "torch_lit")),
"It catches.", "too damp");
GameContext ctx = ctx(gated);
ctx.getState().set("dry");
give(ctx, "matches");
give(ctx, "torch");
Combinations.tryUse(ctx, "matches", "torch");
assertThat(ctx.getPlayer().hasItem("lit_torch")).isTrue();
assertThat(ctx.getState().isSet("torch_lit")).isTrue();
}
}