feat(save): SaveCodec capture/apply over a fresh world; tolerate null collections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package thb.jeanluc.adventure.save;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import thb.jeanluc.adventure.game.GameSession;
|
||||
import thb.jeanluc.adventure.model.Direction;
|
||||
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 thb.jeanluc.adventure.model.item.SwitchableItem;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SaveCodecTest {
|
||||
|
||||
/** Builds a fresh 2-room world: shovel in cellar, lamp (off) in kitchen. */
|
||||
private GameSession freshWorld(String slot) {
|
||||
Room kitchen = new Room("kitchen", "Kitchen", "k");
|
||||
Room cellar = new Room("cellar", "Cellar", "c");
|
||||
kitchen.addExit(Direction.SOUTH, cellar);
|
||||
Item shovel = PlainItem.builder().id("shovel").name("Shovel").description("d").light(false).build();
|
||||
SwitchableItem lamp = SwitchableItem.builder()
|
||||
.id("lamp").name("Lamp").description("d").light(true)
|
||||
.onText("on").offText("off").state(false).effects(List.of()).build();
|
||||
cellar.addItem(shovel);
|
||||
kitchen.addItem(lamp);
|
||||
Map<String, Item> items = new LinkedHashMap<>();
|
||||
items.put("shovel", shovel);
|
||||
items.put("lamp", lamp);
|
||||
Map<String, Room> rooms = new LinkedHashMap<>();
|
||||
rooms.put("kitchen", kitchen);
|
||||
rooms.put("cellar", cellar);
|
||||
World w = new World(rooms, items, Map.of(), "Haunted Manor", "welcome");
|
||||
return new GameSession(w, new Player(kitchen, 0), slot);
|
||||
}
|
||||
|
||||
@Test
|
||||
void captureThenApplyToFreshWorldReproducesState() {
|
||||
GameSession src = freshWorld("slot-a");
|
||||
// mutate: move to cellar, pick up shovel, light the lamp, set a flag, gold
|
||||
src.getPlayer().setCurrentRoom(src.getWorld().getRooms().get("cellar"));
|
||||
Item shovel = src.getWorld().getRooms().get("cellar").removeItem("shovel").orElseThrow();
|
||||
src.getPlayer().addItem(shovel);
|
||||
((SwitchableItem) src.getWorld().getItems().get("lamp")).setState(true);
|
||||
src.getState().set("power_on");
|
||||
src.getPlayer().setGold(5);
|
||||
src.setTurn(12);
|
||||
|
||||
SaveData data = SaveCodec.capture(src);
|
||||
|
||||
GameSession dst = freshWorld("slot-a");
|
||||
SaveCodec.apply(data, dst);
|
||||
|
||||
assertThat(dst.getPlayer().getCurrentRoom().getId()).isEqualTo("cellar");
|
||||
assertThat(dst.getPlayer().hasItem("shovel")).isTrue();
|
||||
assertThat(dst.getWorld().getRooms().get("cellar").findItem("shovel")).isEmpty();
|
||||
assertThat(((SwitchableItem) dst.getWorld().getItems().get("lamp")).isOn()).isTrue();
|
||||
assertThat(dst.getState().isSet("power_on")).isTrue();
|
||||
assertThat(dst.getPlayer().getGold()).isEqualTo(5);
|
||||
assertThat(dst.getTurn()).isEqualTo(12);
|
||||
assertThat(dst.getPlayer().getVisitedRoomIds()).contains("kitchen", "cellar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyRejectsUnknownRoom() {
|
||||
GameSession src = freshWorld("slot-a");
|
||||
SaveData data = SaveCodec.capture(src);
|
||||
SaveData broken = new SaveData(
|
||||
data.schemaVersion(), data.worldTitle(), data.slotName(),
|
||||
data.savedAtEpochMillis(), data.turn(), "ballroom",
|
||||
data.visitedRoomIds(), data.gold(), data.inventoryItemIds(),
|
||||
data.flags(), data.questStages(), data.questCompleted(),
|
||||
data.roomItemIds(), data.switchStates());
|
||||
org.junit.jupiter.api.Assertions.assertThrows(SaveException.class,
|
||||
() -> SaveCodec.apply(broken, freshWorld("slot-a")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyToleratesMissingCollections() {
|
||||
// Construct a SaveData with null collections (simulates a hand-edited / truncated save).
|
||||
// The compact constructor should normalize nulls to empty collections.
|
||||
SaveData data = new SaveData(
|
||||
SaveData.CURRENT_VERSION, "Haunted Manor", "slot-a",
|
||||
System.currentTimeMillis(), 0,
|
||||
"kitchen",
|
||||
null, // visitedRoomIds
|
||||
0,
|
||||
null, // inventoryItemIds
|
||||
null, // flags
|
||||
null, // questStages
|
||||
null, // questCompleted
|
||||
null, // roomItemIds
|
||||
null // switchStates
|
||||
);
|
||||
|
||||
GameSession dst = freshWorld("slot-a");
|
||||
// Should not throw NPE; world items remain in their default positions or cleared
|
||||
org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> SaveCodec.apply(data, dst));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user