Even more simplification and documentation

This commit is contained in:
Nico Marniok 2022-08-31 16:48:18 +02:00
parent 976acb8d41
commit 3036fd5619
11 changed files with 458 additions and 364 deletions

View file

@ -12,7 +12,6 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
import game.logic.GameLogic; import game.logic.GameLogic;
import game.logic.Parser; import game.logic.Parser;
import game.state.CircularLocationException; import game.state.CircularLocationException;
import game.state.Entity;
import startup.Environment; import startup.Environment;
import startup.LoadStuff; import startup.LoadStuff;
@ -26,8 +25,8 @@ public class Main {
Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build(), Engine.newBuilder().option("engine.WarnInterpreterOnly", "false").build(),
Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowHostClassLookup(s -> true)); Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowHostClassLookup(s -> true));
try { try {
Entity t = new Entity("test"); // Entity t = new Entity("test");
jse.put("test", t); jse.put("test", new Object());
jse.eval("console.log(test.toString());"); jse.eval("console.log(test.toString());");
} catch (ScriptException e) { } catch (ScriptException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
@ -36,7 +35,8 @@ public class Main {
Parser parser = new Parser(); Parser parser = new Parser();
try (GameLogic logic = new GameLogic(parser)) { try (GameLogic logic = new GameLogic(parser)) {
logic.loadGameState("games/damnCoolTextAdventureFTW.json"); logic.loadGameState("games/damnCoolTextAdventureFTW/game.json",
"games/damnCoolTextAdventureFTW/initialSave.json");
logic.mainLoop(); logic.mainLoop();
} catch (CircularLocationException ex) { } catch (CircularLocationException ex) {
System.err.println("You messed up you game state: " + ex.getMessage()); System.err.println("You messed up you game state: " + ex.getMessage());

View file

@ -7,7 +7,9 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; 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.CircularLocationException;
import game.state.Entity; import game.state.Entity;
import game.state.EntitySet; import game.state.EntitySet;
@ -15,128 +17,111 @@ import game.state.GameState;
public class GameLogic implements Closeable { public class GameLogic implements Closeable {
private final Parser parser; private final Parser parser;
private GameState state; private GameState gameState;
private Entity player; private Entity player;
private boolean discontinue = false; private boolean discontinue = false;
private Map<String, List<PlayerAction>> playerActions = new HashMap<>(); private Map<String, List<Action>> playerActions = new HashMap<>();
public GameLogic(Parser parser) { public GameLogic(Parser parser) {
this.parser = parser; this.parser = parser;
this.state = new GameState();
} }
public void loadGameState(String stateDescJsonFilePath) throws CircularLocationException { public void loadGameState(String gameDescriptionJsonPath, String savegameJsonPath) throws CircularLocationException {
//////////////////////////////////////////////////////////////////////// this.gameState = new GameState(savegameJsonPath);
// TODO setup code, load from json or so
////////////////////////////////////////////////////////////////////////
Entity east = this.state.createEntity("east"); /*
Entity west = this.state.createEntity("west"); * TODO: load from game specific JSON
Entity inside = this.state.createEntity("inside"); */
Entity outside = this.state.createEntity("outside"); EntitySet collectibles = this.gameState.getEntitySetById("collectibles");
EntitySet genericDirections = EntitySet.createPersistent("genericDirections"); EntitySet genericDirections = this.gameState.getEntitySetById("genericDirections");
genericDirections.add(east, west); EntitySet houseDirections = this.gameState.getEntitySetById("houseDirections");
EntitySet houseDirections = EntitySet.createPersistent("houseDirections"); this.player = this.gameState.getEntityById("player");
houseDirections.add(inside, outside); 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"); ActionSignature takeSignature = new ActionSignature();
Entity clearing = this.state.createEntity("clearing"); takeSignature.pushEntry(collectibles, 1);
Entity houseOutside = this.state.createEntity("house_outside"); this.createAction("take", takeSignature, (logic, actor, args) -> {
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) -> {
Entity collectible = args[0]; Entity collectible = args[0];
if (logic.getPlayer().getLocation() == collectible.getLocation()) { if (actor.getLocation() == collectible.getLocation()) {
try { try {
collectible.setLocation(logic.getPlayer()); collectible.setLocation(actor);
logic.printRaw("Du nimmst %s, du Schuft.\n", collectible); logic.printRaw("%s: Du nimmst %s, du Schuft.\n", actor, collectible);
return true;
} catch (CircularLocationException ex) { } catch (CircularLocationException ex) {
// Should not happpen // Should not happpen
ex.printStackTrace(); ex.printStackTrace();
} }
} }
return true; return false;
}); });
// this.player becomes first argument by calling pushPlayerAction on it ActionSignature goActionSignature = new ActionSignature();
PlayerAction goAction = this.player.pushPlayerAction("go", (logic, args) -> { goActionSignature.pushEntry(genericDirections, 1);
Entity character = args[0]; this.createAction("go", goActionSignature, (logic, actor, args) -> {
Entity direction = args[1]; Entity direction = args[0];
Entity newLocation = character.getLocation().getConnectedEntity(direction); Entity newLocation = actor.getLocation().getConnectedEntity(direction);
if (newLocation != null) { if (newLocation != null) {
try { try {
logic.getPlayer().setLocation(newLocation); 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) { } catch (CircularLocationException ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
} else { } 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; 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 ActionSignature goHouseActionSignature = new ActionSignature();
PlayerAction goHouseAction = this.player.pushPlayerAction("go", (logic, args) -> { goHouseActionSignature.pushEntry(houseDirections, 1);
Entity character = args[0]; this.createAction("go", goHouseActionSignature, (logic, actor, args) -> {
Entity direction = args[1]; Entity direction = args[0];
Entity newLocation = character.getLocation().getConnectedEntity(direction); Entity newLocation = actor.getLocation().getConnectedEntity(direction);
if (newLocation != null && character.getLocation() == houseInside if (newLocation != null && actor.getLocation() == houseInside || actor.getLocation() == houseOutside) {
|| character.getLocation() == houseOutside) {
if (houseMainDoor.getBoolAttribute("open")) { if (houseMainDoor.getBoolAttribute("open")) {
try { try {
character.setLocation(newLocation); actor.setLocation(newLocation);
} catch (CircularLocationException ex) { } catch (CircularLocationException ex) {
ex.printStackTrace(); 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 { } else {
logic.printRaw("Die Tür ist zu, du Dödel.\n"); logic.printRaw("%s: %s ist zu, du Dödel.\n", actor, houseMainDoor);
}
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");
} }
return true; return true;
} }
return false; return false;
}); });
houseMainDoor.pushPlayerAction("close", (logic, args) -> { ActionSignature openCloseSignature = new ActionSignature();
if (logic.getPlayer().getLocation() == houseInside || logic.getPlayer().getLocation() == houseOutside) { openCloseSignature.pushEntry(houseMainDoor);
if (!houseMainDoor.getBoolAttribute("open")) {
logic.printRaw("Die Tür ist schon geschlossen, du Mummenschanz.\n"); 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 { } else {
houseMainDoor.setAttribute("open", false); door.setAttribute("open", true);
logic.printRaw("Du schließt die Tür, du Angsthase.\n"); 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; return true;
} }
@ -145,7 +130,7 @@ public class GameLogic implements Closeable {
} }
public GameState getState() { public GameState getState() {
return this.state; return this.gameState;
} }
public Entity getPlayer() { public Entity getPlayer() {
@ -162,23 +147,24 @@ public class GameLogic implements Closeable {
System.out.printf(rawMessage, args); System.out.printf(rawMessage, args);
} }
public void registerPlayerAction(PlayerAction action) { public void createAction(String id, ActionSignature signature, ActionExecutor executor) {
List<PlayerAction> l = this.playerActions.get(action.getId()); List<Action> l = this.playerActions.get(id);
if (l == null) { 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 * Searches for the first player action that does not use any entity and
* executes it if one is found. * 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 <code>true</code>, if an action was found and executed * @return <code>true</code>, if an action was found and executed
*/ */
public boolean tryExecutePlayerAction(String id) { public boolean tryExecuteAction(String id, Entity actor) {
return this.tryExecutePlayerAction(id, null); return this.tryExecuteAction(id, actor, null);
} }
/** /**
@ -186,14 +172,15 @@ public class GameLogic implements Closeable {
* entities and executes it if one is found. * entities 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
* @param entities The entities on which the action shoud operate * @param entities The entities on which the action shoud operate
* @return <code>true</code>, if an action was found and executed * @return <code>true</code>, if an action was found and executed
*/ */
public boolean tryExecutePlayerAction(String id, EntitySet entities) { public boolean tryExecuteAction(String id, Entity actor, EntitySet entities) {
List<PlayerAction> l = this.playerActions.get(id); List<Action> l = this.playerActions.get(id);
if (l != null) { if (l != null) {
for (PlayerAction a : l) { for (Action a : l) {
if (a.tryExecute(null, entities, this)) { if (a.tryExecute(actor, entities, this)) {
return true; return true;
} }
} }

View file

@ -22,7 +22,7 @@ public class Parser implements Closeable {
String greenPrompt = TextColors.BLUE.colorize(">"); String greenPrompt = TextColors.BLUE.colorize(">");
System.out.printf("%s ", greenPrompt); System.out.printf("%s ", greenPrompt);
List<String> input = Arrays.stream(scanner.nextLine().split("\\s+")).map(String::toLowerCase).toList(); List<String> input = Arrays.asList(scanner.nextLine().split("\\s+"));
if (!input.isEmpty()) { if (!input.isEmpty()) {
String actionId = input.get(0); String actionId = input.get(0);
List<Entity> args = new ArrayList<>(input.size() - 1); List<Entity> args = new ArrayList<>(input.size() - 1);
@ -34,23 +34,12 @@ public class Parser implements Closeable {
} }
args.add(e); args.add(e);
} }
Entity primaryEntity = this.getPrimaryEntity(logic, actionId, args); if (!logic.tryExecuteAction(actionId, logic.getPlayer(), new EntitySet(args))) {
if (primaryEntity != null) {
primaryEntity.tryExecutePlayerAction(actionId, EntitySet.createTemporary(args), logic);
} else if(logic.tryExecutePlayerAction(actionId, EntitySet.createTemporary(args))) {
logic.printRaw("Das geht doch so nicht.\n", args); logic.printRaw("Das geht doch so nicht.\n", args);
} }
} }
} }
private Entity getPrimaryEntity(GameLogic logic, String actionId, List<Entity> arguments) {
return switch (actionId) {
case "go" -> logic.getPlayer();
case "take", "open", "close" -> arguments.remove(0);
default -> null;
};
}
public void parse(List<String> parameter) { public void parse(List<String> parameter) {
String command = parameter.get(0); String command = parameter.get(0);

View file

@ -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 <code>primaryEntity</code> was not
* <code>null</code>)
* @param logic The game logic
* @return <code>true</code>, if the given entities match the signature and the
* action was executed successfully
*/
public boolean tryExecute(Entity actor, EntitySet entities, GameLogic logic) {
List<Entity> matched = this.signatureMatcher.tryMatch(entities);
return matched != null && this.executor.execute(logic, actor, matched.toArray(new Entity[0]));
}
}

View file

@ -4,16 +4,17 @@ import game.logic.GameLogic;
import game.state.Entity; import game.state.Entity;
/** /**
* A player action executor provides specific instructions how to manipulate the * An action executor provides specific instructions how to manipulate the game
* game logic. * logic.
*/ */
public interface PlayerActionExecutor { public interface ActionExecutor {
/** /**
* Executes the game logic manupulation. * Executes the game logic manupulation.
* *
* @param logic The game logic * @param logic The game logic
* @param actor The actor
* @param args Arguments * @param args Arguments
*/ */
public boolean execute(GameLogic logic, Entity... args); public boolean execute(GameLogic logic, Entity actor, Entity... args);
} }

View file

@ -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<SignatureEntry> 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<Entity> getEntryEntitySet(int entryIndex) {
return this.entries.get(entryIndex).entities().getAll();
}
}

View file

@ -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 <code>null</code>
* if no match was found
*/
public List<Entity> 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 <code>true</code> if match was found
*/
private boolean match(int idx) {
if (idx == this.matchedEntities.length) {
return true;
}
int currentEntryIndex = this.signatureEntries[idx];
Set<Entity> allowedEntities = this.signature.getEntryEntitySet(currentEntryIndex);
List<Entity> 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;
}
}

View file

@ -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. <code>"combine"</code>, 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 <a href="#EntitySet"><code>EntitySet</code></a> 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<SignatureEntry> 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
* <a href="#tryExecute">tryExecute</a> 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
* <code>primaryEntity</code> was not
* <code>null</code>)
* @param logic The game logic
* @return <code>true</code>, 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);
}
}

View file

@ -3,15 +3,9 @@ package game.state;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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. * Entities are the building blocks of the game logic's world.
* *
@ -23,17 +17,28 @@ import game.logic.actionsystem.PlayerActionExecutor;
* can store generic attributes. * can store generic attributes.
*/ */
public class Entity { public class Entity {
private final GameState gameState;
private final String id; private final String id;
final Set<EntitySet> containingPersistentSets = new HashSet<>(); final Set<EntitySet> containingPersistentSets = new HashSet<>();
private final Map<String, String> attributes = new HashMap<>(); private final Map<String, String> attributes = new HashMap<>();
private Entity location; private Entity location;
private final EntitySet contents; private final EntitySet contents;
private final Map<Entity, Entity> connections = new HashMap<>(); private final Map<Entity, Entity> connections = new HashMap<>();
private final Map<String, List<PlayerAction>> 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.id = id;
this.contents = EntitySet.createPersistent(this.id + "::contents"); this.gameState.registerEntity(this);
this.contents = this.gameState.createEntitySet(this.id + "::contents");
} }
public String getId() { public String getId() {
@ -81,17 +86,6 @@ public class Entity {
return this.contents.getAll().stream().anyMatch(e -> e == other || (recursive && e.contains(other, true))); 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() { public Entity getLocation() {
return this.location; return this.location;
} }
@ -128,60 +122,6 @@ public class Entity {
return Collections.unmodifiableSet(this.connections.keySet()); return Collections.unmodifiableSet(this.connections.keySet());
} }
public PlayerAction pushPlayerAction(String id, PlayerActionExecutor executor) {
List<PlayerAction> 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 <code>true</code>, 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 <code>true</code>, if an action was found and executed
*/
public boolean tryExecutePlayerAction(String id, GameLogic logic) {
return this.tryExecutePlayerAction(id, null, logic);
}
private boolean tryExecutePlayerAction(List<PlayerAction> actions, EntitySet secondaryEntities, GameLogic logic) {
if (actions != null) {
for (PlayerAction a : actions) {
if (a.tryExecute(this, secondaryEntities, logic)) {
return true;
}
}
}
return false;
}
@Override @Override
public String toString() { public String toString() {
return this.id; return this.id;

View file

@ -3,42 +3,55 @@ package game.state;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate;
import game.logic.GameLogic; import game.logic.GameLogic;
import game.logic.actionsystem.PlayerAction;
import game.logic.actionsystem.PlayerActionExecutor;
public class EntitySet { public class EntitySet {
public static EntitySet createTemporary(Entity... entities) { private final GameState gameState;
return new EntitySet(null, Arrays.asList(entities)); private final String id;
}
public static EntitySet createTemporary(Collection<Entity> 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<Entity> entities) {
return new EntitySet(name, entities);
}
private final String name;
private final Set<Entity> entities = new HashSet<>(); private final Set<Entity> entities = new HashSet<>();
private final Map<String, List<PlayerAction>> playerActions = new HashMap<>();
private EntitySet(String name, Collection<Entity> entities) { /**
this.name = name; * Creates a persistent, named entity set. DO NOT CALL THIS CONSTRUCTOR
this.add(entities.toArray(new Entity[0])); * 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<Entity> 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() { public boolean isEmpty() {
@ -53,17 +66,21 @@ public class EntitySet {
return this.entities.size(); return this.entities.size();
} }
public void add(Entity... entities) { public void add(Collection<Entity> entities) {
for (Entity e : 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); e.containingPersistentSets.add(this);
} }
} }
} }
public void add(Entity... entities) {
this.add(Arrays.asList(entities));
}
public void remove(Entity... entities) { public void remove(Entity... entities) {
for (Entity e : 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); e.containingPersistentSets.remove(this);
} }
} }
@ -73,6 +90,18 @@ public class EntitySet {
return this.entities.contains(entity); 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) { public Entity collapse(GameLogic logic) {
if (this.entities.size() <= 1) { if (this.entities.size() <= 1) {
return this.entities.stream().findAny().orElse(null); return this.entities.stream().findAny().orElse(null);
@ -81,24 +110,4 @@ public class EntitySet {
return this.entities.stream().findAny().orElse(null); return this.entities.stream().findAny().orElse(null);
} }
} }
public EntitySet getFiltered(Predicate<Entity> acceptFunction) {
return EntitySet.createTemporary(this.entities.stream().filter(acceptFunction).toList());
}
public PlayerAction pushPlayerAction(String id, PlayerActionExecutor executor) {
List<PlayerAction> 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<PlayerAction> getPlayerActions(String id) {
List<PlayerAction> l = this.playerActions.get(id);
return l == null ? Collections.emptyList() : Collections.unmodifiableList(this.playerActions.get(id));
}
} }

View file

@ -5,18 +5,82 @@ import java.util.Map;
public class GameState { public class GameState {
private final Map<String, Entity> entities = new HashMap<>(); private final Map<String, Entity> entities = new HashMap<>();
private final Map<String, EntitySet> 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) { public Entity createEntity(String id) {
Entity e = new Entity(id); if (this.entities.containsKey(id)) {
this.entities.put(id, e); throw new UnsupportedOperationException("duplicate entity id: " + id);
return e; }
return new Entity(this, id);
} }
public Entity getEntityById(String id) { public Entity getEntityById(String id) {
return this.entities.get(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);
}
} }