feat(game): Combinations engine for use X on Y
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package thb.jeanluc.adventure.game;
|
||||
|
||||
import thb.jeanluc.adventure.model.Combination;
|
||||
import thb.jeanluc.adventure.model.item.Item;
|
||||
|
||||
/**
|
||||
* Applies {@code use X on Y} item combinations. Stateless utility, mirroring
|
||||
* {@link Conditions}/{@link Effects}/{@link Light}. Operands resolve from the
|
||||
* player's inventory or the current room.
|
||||
*/
|
||||
public final class Combinations {
|
||||
|
||||
private Combinations() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to combine the two named items. Prints a resolution error if an
|
||||
* operand is absent, the recipe's {@code failText} (or a generic line) if a
|
||||
* recipe's requirements are unmet, "Nothing happens." if there is no recipe,
|
||||
* or applies consume/produce/effects + the response on success.
|
||||
*/
|
||||
public static void tryUse(GameContext ctx, String idX, String idY) {
|
||||
if (!present(ctx, idX)) {
|
||||
ctx.getIo().write(noSuch(idX));
|
||||
return;
|
||||
}
|
||||
if (!present(ctx, idY)) {
|
||||
ctx.getIo().write(noSuch(idY));
|
||||
return;
|
||||
}
|
||||
Combination c = ctx.getWorld().getCombinations().get(Combination.key(idX, idY));
|
||||
if (c == null) {
|
||||
ctx.getIo().write("Nothing happens.");
|
||||
return;
|
||||
}
|
||||
if (!Conditions.all(c.requires(), ctx)) {
|
||||
ctx.getIo().write(c.failText() != null ? c.failText() : "Nothing happens.");
|
||||
return;
|
||||
}
|
||||
for (String id : c.consume()) {
|
||||
removeFromAnywhere(ctx, id);
|
||||
}
|
||||
if (c.produce() != null) {
|
||||
Item produced = ctx.getWorld().getItems().get(c.produce());
|
||||
if (produced != null) {
|
||||
ctx.getPlayer().addItem(produced);
|
||||
}
|
||||
}
|
||||
Effects.applyAll(c.effects(), ctx);
|
||||
ctx.getIo().write(c.response());
|
||||
}
|
||||
|
||||
private static boolean present(GameContext ctx, String id) {
|
||||
return ctx.getPlayer().hasItem(id)
|
||||
|| ctx.getPlayer().getCurrentRoom().findItem(id).isPresent();
|
||||
}
|
||||
|
||||
private static void removeFromAnywhere(GameContext ctx, String id) {
|
||||
if (ctx.getPlayer().removeItem(id).isEmpty()) {
|
||||
ctx.getPlayer().getCurrentRoom().removeItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static String noSuch(String id) {
|
||||
return "There is no '" + id + "' here or in your inventory.";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user