From 3036fd561918a5b980a345ed0c3b770469a7b7af Mon Sep 17 00:00:00 2001 From: Nico Marniok Date: Wed, 31 Aug 2022 16:48:18 +0200 Subject: [PATCH] Even more simplification and documentation --- src/main/java/Main.java | 8 +- src/main/java/game/logic/GameLogic.java | 173 ++++++++---------- src/main/java/game/logic/Parser.java | 15 +- .../java/game/logic/actionsystem/Action.java | 64 +++++++ ...ctionExecutor.java => ActionExecutor.java} | 9 +- .../logic/actionsystem/ActionSignature.java | 83 +++++++++ .../actionsystem/ActionSignatureMatcher.java | 78 ++++++++ .../game/logic/actionsystem/PlayerAction.java | 121 ------------ src/main/java/game/state/Entity.java | 88 ++------- src/main/java/game/state/EntitySet.java | 111 +++++------ src/main/java/game/state/GameState.java | 72 +++++++- 11 files changed, 458 insertions(+), 364 deletions(-) create mode 100644 src/main/java/game/logic/actionsystem/Action.java rename src/main/java/game/logic/actionsystem/{PlayerActionExecutor.java => ActionExecutor.java} (50%) create mode 100644 src/main/java/game/logic/actionsystem/ActionSignature.java create mode 100644 src/main/java/game/logic/actionsystem/ActionSignatureMatcher.java delete mode 100644 src/main/java/game/logic/actionsystem/PlayerAction.java diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 03630a2..567ee34 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -12,7 +12,6 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; import game.logic.GameLogic; import game.logic.Parser; import game.state.CircularLocationException; -import game.state.Entity; import startup.Environment; import startup.LoadStuff; @@ -26,8 +25,8 @@ public class Main { Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build(), Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowHostClassLookup(s -> true)); try { - Entity t = new Entity("test"); - jse.put("test", t); + // Entity t = new Entity("test"); + jse.put("test", new Object()); jse.eval("console.log(test.toString());"); } catch (ScriptException e) { // TODO Auto-generated catch block @@ -36,7 +35,8 @@ public class Main { Parser parser = new Parser(); try (GameLogic logic = new GameLogic(parser)) { - logic.loadGameState("games/damnCoolTextAdventureFTW.json"); + logic.loadGameState("games/damnCoolTextAdventureFTW/game.json", + "games/damnCoolTextAdventureFTW/initialSave.json"); logic.mainLoop(); } catch (CircularLocationException ex) { System.err.println("You messed up you game state: " + ex.getMessage()); diff --git a/src/main/java/game/logic/GameLogic.java b/src/main/java/game/logic/GameLogic.java index 65a0a0e..e1d9c63 100644 --- a/src/main/java/game/logic/GameLogic.java +++ b/src/main/java/game/logic/GameLogic.java @@ -7,7 +7,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import game.logic.actionsystem.PlayerAction; +import game.logic.actionsystem.Action; +import game.logic.actionsystem.ActionExecutor; +import game.logic.actionsystem.ActionSignature; import game.state.CircularLocationException; import game.state.Entity; import game.state.EntitySet; @@ -15,128 +17,111 @@ import game.state.GameState; public class GameLogic implements Closeable { private final Parser parser; - private GameState state; + private GameState gameState; private Entity player; private boolean discontinue = false; - private Map> playerActions = new HashMap<>(); + private Map> playerActions = new HashMap<>(); public GameLogic(Parser parser) { this.parser = parser; - this.state = new GameState(); } - public void loadGameState(String stateDescJsonFilePath) throws CircularLocationException { - //////////////////////////////////////////////////////////////////////// - // TODO setup code, load from json or so - //////////////////////////////////////////////////////////////////////// + public void loadGameState(String gameDescriptionJsonPath, String savegameJsonPath) throws CircularLocationException { + this.gameState = new GameState(savegameJsonPath); - Entity east = this.state.createEntity("east"); - Entity west = this.state.createEntity("west"); - Entity inside = this.state.createEntity("inside"); - Entity outside = this.state.createEntity("outside"); - EntitySet genericDirections = EntitySet.createPersistent("genericDirections"); - genericDirections.add(east, west); - EntitySet houseDirections = EntitySet.createPersistent("houseDirections"); - houseDirections.add(inside, outside); + /* + * TODO: load from game specific JSON + */ + EntitySet collectibles = this.gameState.getEntitySetById("collectibles"); + EntitySet genericDirections = this.gameState.getEntitySetById("genericDirections"); + EntitySet houseDirections = this.gameState.getEntitySetById("houseDirections"); + this.player = this.gameState.getEntityById("player"); + Entity houseInside = this.gameState.getEntityById("houseInside"); + Entity houseOutside = this.gameState.getEntityById("houseOutside"); + Entity houseMainDoor = this.gameState.getEntityById("houseMainDoor"); - Entity forestPath01 = this.state.createEntity("forest_path_01"); - Entity clearing = this.state.createEntity("clearing"); - Entity houseOutside = this.state.createEntity("house_outside"); - Entity houseInside = this.state.createEntity("house_inside"); - Entity houseMainDoor = this.state.createEntity("house_main_door"); - - EntitySet locations = EntitySet.createPersistent("locations"); - locations.add(forestPath01, clearing, houseInside, houseOutside); - - forestPath01.connectBidirectional(east, west, clearing); - forestPath01.connectBidirectional(west, east, houseOutside); - houseOutside.connectBidirectional(inside, outside, houseInside); - - this.player = clearing.createContainedEntity(this, "player"); - Entity apple01 = forestPath01.createContainedEntity(this, "apple_01"); - Entity apple02 = forestPath01.createContainedEntity(this, "apple_02"); - - EntitySet collectibles = EntitySet.createPersistent("collectibles"); - collectibles.add(apple01, apple02); - - collectibles.pushPlayerAction("take", (logic, args) -> { + ActionSignature takeSignature = new ActionSignature(); + takeSignature.pushEntry(collectibles, 1); + this.createAction("take", takeSignature, (logic, actor, args) -> { Entity collectible = args[0]; - if (logic.getPlayer().getLocation() == collectible.getLocation()) { + if (actor.getLocation() == collectible.getLocation()) { try { - collectible.setLocation(logic.getPlayer()); - logic.printRaw("Du nimmst %s, du Schuft.\n", collectible); + collectible.setLocation(actor); + logic.printRaw("%s: Du nimmst %s, du Schuft.\n", actor, collectible); + return true; } catch (CircularLocationException ex) { // Should not happpen ex.printStackTrace(); } } - return true; + return false; }); - // this.player becomes first argument by calling pushPlayerAction on it - PlayerAction goAction = this.player.pushPlayerAction("go", (logic, args) -> { - Entity character = args[0]; - Entity direction = args[1]; - Entity newLocation = character.getLocation().getConnectedEntity(direction); + ActionSignature goActionSignature = new ActionSignature(); + goActionSignature.pushEntry(genericDirections, 1); + this.createAction("go", goActionSignature, (logic, actor, args) -> { + Entity direction = args[0]; + Entity newLocation = actor.getLocation().getConnectedEntity(direction); if (newLocation != null) { try { logic.getPlayer().setLocation(newLocation); - logic.printRaw("Du gehst Richtung %s und landest hier: %s, du Lutscher!\n", direction, newLocation); + logic.printRaw("%s: Du gehst Richtung %s und landest hier: %s, du Lutscher!\n", actor, direction, + newLocation); } catch (CircularLocationException ex) { ex.printStackTrace(); } } else { - logic.printRaw("Hier geht es nicht nach %s, du Nichtsnutz.\n", direction); + logic.printRaw("%s: Hier geht es nicht nach %s, du Nichtsnutz.\n", actor, direction); } return true; }); - // second argument must be exactly 1 of the entites contained in genericDirections - goAction.pushVaryingNeededEntites(genericDirections, 1); - // again first argument becomes this.player by calling pushPlayerAction on it - PlayerAction goHouseAction = this.player.pushPlayerAction("go", (logic, args) -> { - Entity character = args[0]; - Entity direction = args[1]; - Entity newLocation = character.getLocation().getConnectedEntity(direction); - if (newLocation != null && character.getLocation() == houseInside - || character.getLocation() == houseOutside) { + ActionSignature goHouseActionSignature = new ActionSignature(); + goHouseActionSignature.pushEntry(houseDirections, 1); + this.createAction("go", goHouseActionSignature, (logic, actor, args) -> { + Entity direction = args[0]; + Entity newLocation = actor.getLocation().getConnectedEntity(direction); + if (newLocation != null && actor.getLocation() == houseInside || actor.getLocation() == houseOutside) { if (houseMainDoor.getBoolAttribute("open")) { try { - character.setLocation(newLocation); + actor.setLocation(newLocation); } catch (CircularLocationException ex) { ex.printStackTrace(); } - logic.printRaw("Du gehst durch die Tür, du Eumel.\n"); + logic.printRaw("%s: Du gehst durch %s, du Eumel.\n", actor, houseMainDoor); } else { - logic.printRaw("Die Tür ist zu, du Dödel.\n"); - } - return true; - } - return false; - }); - // second argument must be exactly 1 of the entites contained in houseDirections - goHouseAction.pushVaryingNeededEntites(houseDirections, 1); - - houseMainDoor.pushPlayerAction("open", (logic, args) -> { - if (logic.getPlayer().getLocation() == houseInside || logic.getPlayer().getLocation() == houseOutside) { - if (houseMainDoor.getBoolAttribute("open")) { - logic.printRaw("Die Tür ist schon offen, du Hammel.\n"); - } else { - houseMainDoor.setAttribute("open", true); - logic.printRaw("Du öffnest die Tür, du Dummbatz.\n"); + logic.printRaw("%s: %s ist zu, du Dödel.\n", actor, houseMainDoor); } return true; } return false; }); - houseMainDoor.pushPlayerAction("close", (logic, args) -> { - if (logic.getPlayer().getLocation() == houseInside || logic.getPlayer().getLocation() == houseOutside) { - if (!houseMainDoor.getBoolAttribute("open")) { - logic.printRaw("Die Tür ist schon geschlossen, du Mummenschanz.\n"); + ActionSignature openCloseSignature = new ActionSignature(); + openCloseSignature.pushEntry(houseMainDoor); + + this.createAction("open", openCloseSignature, (logic, actor, args) -> { + Entity door = args[0]; + if (actor.getLocation() == houseInside || actor.getLocation() == houseOutside) { + if (door.getBoolAttribute("open")) { + logic.printRaw("%s: Die Tür ist schon offen, du Hammel.\n", actor); } else { - houseMainDoor.setAttribute("open", false); - logic.printRaw("Du schließt die Tür, du Angsthase.\n"); + door.setAttribute("open", true); + logic.printRaw("%s: Du öffnest die Tür, du Dummbatz.\n", actor); + } + return true; + } + return false; + }); + + this.createAction("close", openCloseSignature, (logic, actor, args) -> { + Entity door = args[0]; + if (actor.getLocation() == houseInside || actor.getLocation() == houseOutside) { + if (!door.getBoolAttribute("open")) { + logic.printRaw("%s: Die Tür ist schon geschlossen, du Mummenschanz.\n", actor); + } else { + door.setAttribute("open", false); + logic.printRaw("%s: Du schließt die Tür, du Angsthase.\n", actor); } return true; } @@ -145,7 +130,7 @@ public class GameLogic implements Closeable { } public GameState getState() { - return this.state; + return this.gameState; } public Entity getPlayer() { @@ -162,23 +147,24 @@ public class GameLogic implements Closeable { System.out.printf(rawMessage, args); } - public void registerPlayerAction(PlayerAction action) { - List l = this.playerActions.get(action.getId()); + public void createAction(String id, ActionSignature signature, ActionExecutor executor) { + List l = this.playerActions.get(id); if (l == null) { - this.playerActions.put(action.getId(), l = new LinkedList<>()); + this.playerActions.put(id, l = new LinkedList<>()); } - l.add(action); + l.add(new Action(id, signature, executor)); } /** * Searches for the first player action that does not use any entity and * executes it if one is found. * - * @param id The action id + * @param id The action id + * @param actor The actor who should execute the action * @return true, if an action was found and executed */ - public boolean tryExecutePlayerAction(String id) { - return this.tryExecutePlayerAction(id, null); + public boolean tryExecuteAction(String id, Entity actor) { + return this.tryExecuteAction(id, actor, null); } /** @@ -186,14 +172,15 @@ public class GameLogic implements Closeable { * entities and executes it if one is found. * * @param id The action id + * @param actor The actor who should execute the action * @param entities The entities on which the action shoud operate * @return true, if an action was found and executed */ - public boolean tryExecutePlayerAction(String id, EntitySet entities) { - List l = this.playerActions.get(id); + public boolean tryExecuteAction(String id, Entity actor, EntitySet entities) { + List l = this.playerActions.get(id); if (l != null) { - for (PlayerAction a : l) { - if (a.tryExecute(null, entities, this)) { + for (Action a : l) { + if (a.tryExecute(actor, entities, this)) { return true; } } diff --git a/src/main/java/game/logic/Parser.java b/src/main/java/game/logic/Parser.java index fee1687..0336133 100644 --- a/src/main/java/game/logic/Parser.java +++ b/src/main/java/game/logic/Parser.java @@ -22,7 +22,7 @@ public class Parser implements Closeable { String greenPrompt = TextColors.BLUE.colorize(">"); System.out.printf("%s ", greenPrompt); - List input = Arrays.stream(scanner.nextLine().split("\\s+")).map(String::toLowerCase).toList(); + List input = Arrays.asList(scanner.nextLine().split("\\s+")); if (!input.isEmpty()) { String actionId = input.get(0); List args = new ArrayList<>(input.size() - 1); @@ -34,23 +34,12 @@ public class Parser implements Closeable { } args.add(e); } - Entity primaryEntity = this.getPrimaryEntity(logic, actionId, args); - if (primaryEntity != null) { - primaryEntity.tryExecutePlayerAction(actionId, EntitySet.createTemporary(args), logic); - } else if(logic.tryExecutePlayerAction(actionId, EntitySet.createTemporary(args))) { + if (!logic.tryExecuteAction(actionId, logic.getPlayer(), new EntitySet(args))) { logic.printRaw("Das geht doch so nicht.\n", args); } } } - private Entity getPrimaryEntity(GameLogic logic, String actionId, List arguments) { - return switch (actionId) { - case "go" -> logic.getPlayer(); - case "take", "open", "close" -> arguments.remove(0); - default -> null; - }; - } - public void parse(List parameter) { String command = parameter.get(0); diff --git a/src/main/java/game/logic/actionsystem/Action.java b/src/main/java/game/logic/actionsystem/Action.java new file mode 100644 index 0000000..5d9535e --- /dev/null +++ b/src/main/java/game/logic/actionsystem/Action.java @@ -0,0 +1,64 @@ +package game.logic.actionsystem; + +import java.util.List; + +import game.logic.GameLogic; +import game.state.Entity; +import game.state.EntitySet; + +/** + * A player action represents a single action which operates on any number of + * entities. + * + * It has an identifier, e.g. {@code "combine"}, a signature of needed entities, + * and an executor. The signature consists of multiple entries, each one being + * either a single entity or a group of entities. To execute an action a + * {@link game.state.EntitySet} must be given which can be matched against its + * signature. + */ +public class Action { + private final String id; + private final ActionExecutor executor; + private final ActionSignatureMatcher signatureMatcher; + + /** + * Constructs a player action by providing an id and an executor. + * + * @param id The action's id + * @param signature The action's signature + * @param executor The executor which is executed if a matching entity set is + * provided to + * {@link Action#tryExecute(Entity, EntitySet, GameLogic)}. + */ + public Action(String id, ActionSignature signature, ActionExecutor executor) { + this.id = id; + this.signatureMatcher = new ActionSignatureMatcher(signature); + this.executor = executor; + } + + /** + * Getter for the action's id. + * + * @return The action id + */ + public String getId() { + return this.id; + } + + /** + * Tries to match the given entities to this action's signature and tries to + * execute it if match was successful. + * + * @param actor The actor which executes the action + * @param entities A set of entities which will be matched against the signature + * (excl. the first entry if primaryEntity was not + * null) + * @param logic The game logic + * @return true, if the given entities match the signature and the + * action was executed successfully + */ + public boolean tryExecute(Entity actor, EntitySet entities, GameLogic logic) { + List matched = this.signatureMatcher.tryMatch(entities); + return matched != null && this.executor.execute(logic, actor, matched.toArray(new Entity[0])); + } +} diff --git a/src/main/java/game/logic/actionsystem/PlayerActionExecutor.java b/src/main/java/game/logic/actionsystem/ActionExecutor.java similarity index 50% rename from src/main/java/game/logic/actionsystem/PlayerActionExecutor.java rename to src/main/java/game/logic/actionsystem/ActionExecutor.java index 2c7bb60..f0cfa84 100644 --- a/src/main/java/game/logic/actionsystem/PlayerActionExecutor.java +++ b/src/main/java/game/logic/actionsystem/ActionExecutor.java @@ -4,16 +4,17 @@ import game.logic.GameLogic; import game.state.Entity; /** - * A player action executor provides specific instructions how to manipulate the - * game logic. + * An action executor provides specific instructions how to manipulate the game + * logic. */ -public interface PlayerActionExecutor { +public interface ActionExecutor { /** * Executes the game logic manupulation. * * @param logic The game logic + * @param actor The actor * @param args Arguments */ - public boolean execute(GameLogic logic, Entity... args); + public boolean execute(GameLogic logic, Entity actor, Entity... args); } diff --git a/src/main/java/game/logic/actionsystem/ActionSignature.java b/src/main/java/game/logic/actionsystem/ActionSignature.java new file mode 100644 index 0000000..281c225 --- /dev/null +++ b/src/main/java/game/logic/actionsystem/ActionSignature.java @@ -0,0 +1,83 @@ +package game.logic.actionsystem; + +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import game.state.Entity; +import game.state.EntitySet; + +/** + * An action signature describes which entities are needed for an action to + * execute. Note that even if the signature matches the execution itself can + * still fail. + */ +public class ActionSignature { + private record SignatureEntry(EntitySet entities, int size) { + } + + private final List entries = new LinkedList<>(); + private int totalEntityCount = 0; + + /** + * Pushes a new entry to the signature consisting of a single entity which is + * needed for execution. + * + * @param entity The needed entity to push + */ + public void pushEntry(Entity entity) { + this.pushEntry(new EntitySet(entity), 1); + } + + /** + * Pushes a new entry to the signature consisting of a set of entities of which + * a specific count is needed for execution. + * + * @param entities The set of needed entities + * @param size How many entities of the given set are needed for execution + */ + public void pushEntry(EntitySet entities, int size) { + this.entries.add(new SignatureEntry(entities, size)); + this.totalEntityCount += size; + } + + /** + * The total entity count is the sum of all entry sizes. + * + * @return The total entity count + */ + public int getTotalEntityCount() { + return this.totalEntityCount; + } + + /** + * The total number of entries pushed to the signature. + * + * @return The entry count + */ + public int getEntryCount() { + return this.entries.size(); + } + + /** + * The size of an entry is the size paramter given when the entry was pushed to + * the signature. + * + * @param entryIndex The index of the entry + * @return The size of the entry + */ + public int getEntrySize(int entryIndex) { + return this.entries.get(entryIndex).size(); + } + + /** + * The allowed entities of an entry were given when the entry was pushed to the + * signature. + * + * @param entryIndex The entry's index + * @return The allowed entities of an entry + */ + public Set getEntryEntitySet(int entryIndex) { + return this.entries.get(entryIndex).entities().getAll(); + } +} diff --git a/src/main/java/game/logic/actionsystem/ActionSignatureMatcher.java b/src/main/java/game/logic/actionsystem/ActionSignatureMatcher.java new file mode 100644 index 0000000..8db39dc --- /dev/null +++ b/src/main/java/game/logic/actionsystem/ActionSignatureMatcher.java @@ -0,0 +1,78 @@ +package game.logic.actionsystem; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import game.state.Entity; +import game.state.EntitySet; + +/** + * The action signature matcher matches a given set of entities against a + * signature. + */ +public class ActionSignatureMatcher { + private final ActionSignature signature; + private final int signatureEntries[]; + private final Entity matchedEntities[]; + private EntitySet workingSet; + + /** + * Constructor + * + * @param signature The signature + */ + public ActionSignatureMatcher(ActionSignature signature) { + this.signature = signature; + this.signatureEntries = new int[signature.getTotalEntityCount()]; + this.matchedEntities = new Entity[signature.getTotalEntityCount()]; + int fillFromIdx = 0; + for (int i = 0; i < signature.getEntryCount(); ++i) { + int fillToIndex = fillFromIdx + signature.getEntrySize(i); + Arrays.fill(this.signatureEntries, fillFromIdx, fillToIndex, i); + fillFromIdx = fillToIndex; + } + } + + /** + * Tries to match the given set of entities to the signature which was provided + * during construction. If a match was found the matching entities are returned + * in the order they match the signature. + * + * @param entities The set of entities to match + * @return The matching entities in order of the signature or null + * if no match was found + */ + public List tryMatch(EntitySet entities) { + int givenEntityCount = entities == null ? 0 : entities.getSize(); + if (givenEntityCount != this.signature.getTotalEntityCount()) { + return null; + } + this.workingSet = new EntitySet(entities.getAll()); + return this.match(0) ? Arrays.asList(this.matchedEntities) : null; + } + + /** + * Recursive matching algorithm. + * + * @param idx The index to start the match + * @return true if match was found + */ + private boolean match(int idx) { + if (idx == this.matchedEntities.length) { + return true; + } + int currentEntryIndex = this.signatureEntries[idx]; + Set allowedEntities = this.signature.getEntryEntitySet(currentEntryIndex); + List matchingEntities = this.workingSet.getAll().stream().filter(allowedEntities::contains).toList(); + for (Entity e : matchingEntities) { + this.workingSet.remove(e); + this.matchedEntities[idx] = e; + if (this.match(idx + 1)) { + return true; + } + this.workingSet.add(e); + } + return false; + } +} diff --git a/src/main/java/game/logic/actionsystem/PlayerAction.java b/src/main/java/game/logic/actionsystem/PlayerAction.java deleted file mode 100644 index 58eb38d..0000000 --- a/src/main/java/game/logic/actionsystem/PlayerAction.java +++ /dev/null @@ -1,121 +0,0 @@ -package game.logic.actionsystem; - -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -import game.logic.GameLogic; -import game.state.Entity; -import game.state.EntitySet; - -/** - * A player action represents a single action which operates on any number of - * entities. - * - * It has an identifier, e.g. "combine", a signature of needed - * entities, and an executor. The signature consists multiple entries, each one - * being either a single entity or a group of entities. To execute a player - * action an EntitySet must be given which - * can be matched against its signature. - */ -public class PlayerAction { - private record SignatureEntry(EntitySet entities, int count) { - } - - private final String id; - private final PlayerActionExecutor executor; - private final List signatureEntries = new LinkedList<>(); - private int neededEntitiesTotalCount = 0; - - /** - * Constructs a player action by providing an id and an executor. - * - * @param id The action id - * @param executor The executor which is called when - * tryExecute is called with an entity - * set which can be matched against the signature. - */ - public PlayerAction(String id, PlayerActionExecutor executor) { - this.id = id; - this.executor = executor; - } - - /** - * Getter for the action's id. - * - * @return The action id - */ - public String getId() { - return this.id; - } - - /** - * Pushes a new entry to this action' signature consisting of a single entity - * which is needed for execution. - * - * @param entity The needed entity to push - */ - public void pushNeededEntity(Entity entity) { - this.signatureEntries.add(new SignatureEntry(EntitySet.createTemporary(entity), 1)); - ++this.neededEntitiesTotalCount; - } - - /** - * Pushes a new entry to this action's signature consisting of a set of entities - * of which a specific count is needed for execution. - * - * @param entities The set of needed entities - * @param count How many entities of the given set are needed for execution - */ - public void pushVaryingNeededEntites(EntitySet entities, int count) { - this.signatureEntries.add(new SignatureEntry(entities, count)); - this.neededEntitiesTotalCount += count; - } - - /** - * Tries to match the given entities to this action's signature and executes it - * if successful. - * - * @param primaryEntity An optional primary entity which will be matched - * against the first signature entry. - * @param secondaryEntities A set of entities which will be matched against the - * signature (excl. the first entry if - * primaryEntity was not - * null) - * @param logic The game logic - * @return true, if given entities match the signature and the - * action was executed - */ - public boolean tryExecute(Entity primaryEntity, EntitySet secondaryEntities, GameLogic logic) { - if ((primaryEntity == null ? 0 : 1) - + (secondaryEntities == null ? 0 : secondaryEntities.getSize()) != this.neededEntitiesTotalCount) { - return false; - } - int entityCounts[] = new int[this.signatureEntries.size()]; - Entity entitiesToUse[] = new Entity[this.neededEntitiesTotalCount]; - if (primaryEntity != null) { - if (this.signatureEntries.isEmpty() || !this.signatureEntries.get(0).entities().contains(primaryEntity)) { - return false; - } - entitiesToUse[entityCounts[0]++] = primaryEntity; - } - if (secondaryEntities != null) { - for (Entity e : secondaryEntities.getAll()) { - int idx = 0; - for (int i = 0; i < this.signatureEntries.size(); ++i) { - SignatureEntry signatureEntry = this.signatureEntries.get(i); - if (entityCounts[i] != signatureEntry.count() && signatureEntry.entities().contains(e)) { - entitiesToUse[idx + entityCounts[i]++] = e; - break; - } - idx += signatureEntry.count(); - } - } - } - if (Arrays.stream(entitiesToUse).anyMatch(Objects::isNull)) { - return false; - } - return this.executor.execute(logic, entitiesToUse); - } -} diff --git a/src/main/java/game/state/Entity.java b/src/main/java/game/state/Entity.java index aa9c773..bc5eb33 100644 --- a/src/main/java/game/state/Entity.java +++ b/src/main/java/game/state/Entity.java @@ -3,15 +3,9 @@ package game.state; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.Set; -import game.logic.GameLogic; -import game.logic.actionsystem.PlayerAction; -import game.logic.actionsystem.PlayerActionExecutor; - /** * Entities are the building blocks of the game logic's world. * @@ -23,17 +17,28 @@ import game.logic.actionsystem.PlayerActionExecutor; * can store generic attributes. */ public class Entity { + private final GameState gameState; private final String id; final Set containingPersistentSets = new HashSet<>(); private final Map attributes = new HashMap<>(); private Entity location; private final EntitySet contents; private final Map connections = new HashMap<>(); - private final Map> playerActions = new HashMap<>(); - public Entity(String id) { + /** + * Creates an entity and registers it to the given game state. DO NOT CALL THIS + * CONSTRUCTOR DIRECTLY! Use {@link GameState#createEntity(String)}, + * {@link EntitySet#createEntity(String)}, or + * {@link EntitySet#createEntity(String, Entity)} instead. + * + * @param state The game sate to register the entity to + * @param id The entity id + */ + public Entity(GameState gameState, String id) { + this.gameState = gameState; this.id = id; - this.contents = EntitySet.createPersistent(this.id + "::contents"); + this.gameState.registerEntity(this); + this.contents = this.gameState.createEntitySet(this.id + "::contents"); } public String getId() { @@ -81,17 +86,6 @@ public class Entity { return this.contents.getAll().stream().anyMatch(e -> e == other || (recursive && e.contains(other, true))); } - public Entity createContainedEntity(GameLogic logic, String id) { - Entity e = logic.getState().createEntity(id); - try { - e.setLocation(this); - } catch (CircularLocationException ex) { - // should not happen - ex.printStackTrace(); - } - return e; - } - public Entity getLocation() { return this.location; } @@ -128,60 +122,6 @@ public class Entity { return Collections.unmodifiableSet(this.connections.keySet()); } - public PlayerAction pushPlayerAction(String id, PlayerActionExecutor executor) { - List l = this.playerActions.get(id); - if (l == null) { - this.playerActions.put(id, l = new LinkedList<>()); - } - PlayerAction action = new PlayerAction(id, executor); - action.pushNeededEntity(this); - l.add(action); - return action; - } - - /** - * Searches for the first player action that can operate on this entity and the - * given secondary entities and executes it if one is found. - * - * @param id The action id - * @param secondaryEntities The set of secondary entities on which the action - * should operate - * @return true, if an action was found and executed - */ - public boolean tryExecutePlayerAction(String id, EntitySet secondaryEntities, GameLogic logic) { - if (!this.tryExecutePlayerAction(this.playerActions.get(id), secondaryEntities, logic)) { - for (EntitySet set : this.containingPersistentSets) { - if (this.tryExecutePlayerAction(set.getPlayerActions(id), secondaryEntities, logic)) { - return true; - } - } - return false; - } - return true; - } - - /** - * Searches for the first player action that can operate on this entity and no - * secondary entities executes it if one is found. - * - * @param id The action id - * @return true, if an action was found and executed - */ - public boolean tryExecutePlayerAction(String id, GameLogic logic) { - return this.tryExecutePlayerAction(id, null, logic); - } - - private boolean tryExecutePlayerAction(List actions, EntitySet secondaryEntities, GameLogic logic) { - if (actions != null) { - for (PlayerAction a : actions) { - if (a.tryExecute(this, secondaryEntities, logic)) { - return true; - } - } - } - return false; - } - @Override public String toString() { return this.id; diff --git a/src/main/java/game/state/EntitySet.java b/src/main/java/game/state/EntitySet.java index 18b282e..b874b84 100644 --- a/src/main/java/game/state/EntitySet.java +++ b/src/main/java/game/state/EntitySet.java @@ -3,42 +3,55 @@ package game.state; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import game.logic.GameLogic; -import game.logic.actionsystem.PlayerAction; -import game.logic.actionsystem.PlayerActionExecutor; public class EntitySet { - public static EntitySet createTemporary(Entity... entities) { - return new EntitySet(null, Arrays.asList(entities)); - } - - public static EntitySet createTemporary(Collection entities) { - return new EntitySet(null, entities); - } - - public static EntitySet createPersistent(String name, Entity... entities) { - return new EntitySet(name, Arrays.asList(entities)); - } - - public static EntitySet createPersistent(String name, Collection entities) { - return new EntitySet(name, entities); - } - - private final String name; + private final GameState gameState; + private final String id; private final Set entities = new HashSet<>(); - private final Map> playerActions = new HashMap<>(); - private EntitySet(String name, Collection entities) { - this.name = name; - this.add(entities.toArray(new Entity[0])); + /** + * Creates a persistent, named entity set. DO NOT CALL THIS CONSTRUCTOR + * DIRECTLY. Use {@link GameState#createEntitySet(String)} instead. + * + * @param state The game state to register the entity set to + * @param id The entity set id + */ + public EntitySet(GameState gameState, String id) { + this.gameState = gameState; + this.id = id; + this.gameState.registerEntitySet(this); + } + + /** + * Creates a temporary entity set. + * + * @param entities Initial entities contained in the set + */ + public EntitySet(Collection entities) { + this.id = null; + this.gameState = null; + this.entities.addAll(entities); + } + + /** + * Creates a temporary entity set. + * + * @param entities Initial entities contained in the set + */ + public EntitySet(Entity... entities) { + this(Arrays.asList(entities)); + } + + public String getId() { + return this.id; + } + + public boolean isPersistent() { + return this.id != null; } public boolean isEmpty() { @@ -53,17 +66,21 @@ public class EntitySet { return this.entities.size(); } - public void add(Entity... entities) { + public void add(Collection entities) { for (Entity e : entities) { - if (this.entities.add(e) && this.name != null) { + if (this.entities.add(e) && this.id != null) { e.containingPersistentSets.add(this); } } } + public void add(Entity... entities) { + this.add(Arrays.asList(entities)); + } + public void remove(Entity... entities) { for (Entity e : entities) { - if (this.entities.remove(e) && this.name != null) { + if (this.entities.remove(e) && this.id != null) { e.containingPersistentSets.remove(this); } } @@ -73,6 +90,18 @@ public class EntitySet { return this.entities.contains(entity); } + public Entity createEntity(String id) { + Entity e = this.gameState.createEntity(id); + this.add(e); + return e; + } + + public Entity createEntity(String id, Entity location) throws CircularLocationException { + Entity e = this.createEntity(id); + e.setLocation(location); + return e; + } + public Entity collapse(GameLogic logic) { if (this.entities.size() <= 1) { return this.entities.stream().findAny().orElse(null); @@ -81,24 +110,4 @@ public class EntitySet { return this.entities.stream().findAny().orElse(null); } } - - public EntitySet getFiltered(Predicate acceptFunction) { - return EntitySet.createTemporary(this.entities.stream().filter(acceptFunction).toList()); - } - - public PlayerAction pushPlayerAction(String id, PlayerActionExecutor executor) { - List l = this.playerActions.get(id); - if (l == null) { - this.playerActions.put(id, l = new LinkedList<>()); - } - PlayerAction action = new PlayerAction(id, executor); - action.pushVaryingNeededEntites(this, 1); - l.add(action); - return action; - } - - public List getPlayerActions(String id) { - List l = this.playerActions.get(id); - return l == null ? Collections.emptyList() : Collections.unmodifiableList(this.playerActions.get(id)); - } } diff --git a/src/main/java/game/state/GameState.java b/src/main/java/game/state/GameState.java index 0bb78a9..633e615 100644 --- a/src/main/java/game/state/GameState.java +++ b/src/main/java/game/state/GameState.java @@ -5,18 +5,82 @@ import java.util.Map; public class GameState { private final Map entities = new HashMap<>(); + private final Map entitySets = new HashMap<>(); - public GameState() { + public GameState(String savegameJsonPath) { + try { + /* + * TODO: load from savegame JSON + */ + EntitySet genericDirections = this.createEntitySet("genericDirections"); + Entity west = genericDirections.createEntity("west"); + Entity east = genericDirections.createEntity("east"); + EntitySet houseDirections = this.createEntitySet("houseDirections"); + Entity inside = houseDirections.createEntity("inside"); + Entity outside = houseDirections.createEntity("outside"); + + EntitySet locations = this.createEntitySet("locations"); + Entity forestPath01 = locations.createEntity("forestPath01"); + Entity clearing = locations.createEntity("clearing"); + Entity houseOutside = locations.createEntity("houseOutside"); + Entity houseInside = locations.createEntity("houseInside"); + locations.createEntity("houseMainDoor"); + + forestPath01.connectBidirectional(east, west, clearing); + forestPath01.connectBidirectional(west, east, houseOutside); + houseOutside.connectBidirectional(inside, outside, houseInside); + + EntitySet characters = this.createEntitySet("characters"); + characters.createEntity("player", clearing); + + EntitySet collectibles = this.createEntitySet("collectibles"); + collectibles.createEntity("apple01", forestPath01); + collectibles.createEntity("apple02", forestPath01); + } catch (CircularLocationException ex) { + ex.printStackTrace(); + } } public Entity createEntity(String id) { - Entity e = new Entity(id); - this.entities.put(id, e); - return e; + if (this.entities.containsKey(id)) { + throw new UnsupportedOperationException("duplicate entity id: " + id); + } + return new Entity(this, id); } public Entity getEntityById(String id) { return this.entities.get(id); } + + public EntitySet createEntitySet(String id) { + if (this.entitySets.containsKey(id)) { + throw new UnsupportedOperationException("duplicate entity set id: " + id); + } + return new EntitySet(this, id); + } + + public EntitySet getEntitySetById(String id) { + return this.entitySets.get(id); + } + + /** + * Registers an entity to the game state. DO NOT CALL THIS OUTSIDE OF THE ENTITY + * CONSTRUCTOR! + * + * @param entity The entity to register + */ + public void registerEntity(Entity entity) { + this.entities.put(entity.getId(), entity); + } + + /** + * Registers an entity set to the game state. DO NOT CALL THIS OUTSIDE OF THE + * ENTITY SET CONSTRUCTOR! + * + * @param entitySet The entity set to register + */ + public void registerEntitySet(EntitySet entitySet) { + this.entitySets.put(entitySet.getId(), entitySet); + } } \ No newline at end of file