mirror of
https://github.com/schmaeddes/untitledTextAdventure.git
synced 2024-11-14 07:38:05 +01:00
Even more simplification and documentation
This commit is contained in:
parent
976acb8d41
commit
3036fd5619
11 changed files with 458 additions and 364 deletions
|
@ -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());
|
||||
|
|
|
@ -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<String, List<PlayerAction>> playerActions = new HashMap<>();
|
||||
private Map<String, List<Action>> 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<PlayerAction> l = this.playerActions.get(action.getId());
|
||||
public void createAction(String id, ActionSignature signature, ActionExecutor executor) {
|
||||
List<Action> 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 <code>true</code>, 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 <code>true</code>, if an action was found and executed
|
||||
*/
|
||||
public boolean tryExecutePlayerAction(String id, EntitySet entities) {
|
||||
List<PlayerAction> l = this.playerActions.get(id);
|
||||
public boolean tryExecuteAction(String id, Entity actor, EntitySet entities) {
|
||||
List<Action> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ public class Parser implements Closeable {
|
|||
|
||||
String greenPrompt = TextColors.BLUE.colorize(">");
|
||||
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()) {
|
||||
String actionId = input.get(0);
|
||||
List<Entity> 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<Entity> arguments) {
|
||||
return switch (actionId) {
|
||||
case "go" -> logic.getPlayer();
|
||||
case "take", "open", "close" -> arguments.remove(0);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public void parse(List<String> parameter) {
|
||||
String command = parameter.get(0);
|
||||
|
||||
|
|
64
src/main/java/game/logic/actionsystem/Action.java
Normal file
64
src/main/java/game/logic/actionsystem/Action.java
Normal 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]));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
83
src/main/java/game/logic/actionsystem/ActionSignature.java
Normal file
83
src/main/java/game/logic/actionsystem/ActionSignature.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<EntitySet> containingPersistentSets = new HashSet<>();
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
private Entity location;
|
||||
private final EntitySet contents;
|
||||
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.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<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
|
||||
public String toString() {
|
||||
return this.id;
|
||||
|
|
|
@ -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<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 GameState gameState;
|
||||
private final String id;
|
||||
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;
|
||||
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<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() {
|
||||
|
@ -53,17 +66,21 @@ public class EntitySet {
|
|||
return this.entities.size();
|
||||
}
|
||||
|
||||
public void add(Entity... entities) {
|
||||
public void add(Collection<Entity> 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<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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,82 @@ import java.util.Map;
|
|||
|
||||
public class GameState {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue