diff --git a/pom.xml b/pom.xml index 9bdeaae..b8cef28 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 17 @@ -11,4 +9,29 @@ schmaeddes untitledTextAdventure 1.0-SNAPSHOT + + + + + + org.graalvm.js + js + 22.2.0 + + + + org.graalvm.js + js-scriptengine + 22.2.0 + + + + org.graalvm.truffle + truffle-api + 22.2.0 + + + + + \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java index f63178c..48c7fc0 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,8 +1,19 @@ import java.io.IOException; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.HostAccess; + +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; @@ -12,6 +23,19 @@ public class Main { LoadStuff loadStuff = new LoadStuff(); loadStuff.load(Environment.instance); + ScriptEngine jse = GraalJSScriptEngine.create( + 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); + jse.eval("console.log(test.toString());"); + } catch (ScriptException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + System.exit(0); Parser parser = new Parser(); try (GameLogic logic = new GameLogic(parser)) { logic.loadGameState("games/damnCoolTextAdventureFTW.json"); diff --git a/src/main/java/game/logic/GameLogic.java b/src/main/java/game/logic/GameLogic.java index 67e3837..37326da 100644 --- a/src/main/java/game/logic/GameLogic.java +++ b/src/main/java/game/logic/GameLogic.java @@ -2,11 +2,14 @@ package game.logic; import java.io.Closeable; import java.io.IOException; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.function.Predicate; import game.logic.actionsystem.Action; +import game.logic.actionsystem.PlayerAction; import game.logic.actionsystem.actions.GoDirection; import game.logic.actionsystem.actions.Open; import game.logic.actionsystem.actions.TakeFrom; @@ -20,6 +23,7 @@ public class GameLogic implements Closeable { private GameState state; private Entity player; private boolean discontinue = false; + private Map> playerActions = new HashMap<>(); public GameLogic(Parser parser) { this.parser = parser; @@ -162,6 +166,45 @@ public class GameLogic implements Closeable { return true; } + public void registerPlayerAction(PlayerAction action) { + List l = this.playerActions.get(action.getId()); + if (l == null) { + this.playerActions.put(action.getId(), l = new LinkedList<>()); + } + l.add(action); + } + + /** + * Searches for the first player action that does not use any entity and + * 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) { + return this.tryExecutePlayerAction(id, null); + } + + /** + * Searches for the first player action that can operate on the given set of + * entities and executes it if one is found. + * + * @param id The action id + * @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); + if (l != null) { + for (PlayerAction a : l) { + if (a.tryExecute(null, entities, this)) { + return true; + } + } + } + return false; + } + @Override public void close() throws IOException { this.parser.close(); diff --git a/src/main/java/game/logic/actionsystem/PlayerAction.java b/src/main/java/game/logic/actionsystem/PlayerAction.java new file mode 100644 index 0000000..af32a4e --- /dev/null +++ b/src/main/java/game/logic/actionsystem/PlayerAction.java @@ -0,0 +1,122 @@ +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; + } + this.executor.execute(logic, entitiesToUse); + return true; + } +} diff --git a/src/main/java/game/logic/actionsystem/PlayerActionExecutor.java b/src/main/java/game/logic/actionsystem/PlayerActionExecutor.java new file mode 100644 index 0000000..dfc766f --- /dev/null +++ b/src/main/java/game/logic/actionsystem/PlayerActionExecutor.java @@ -0,0 +1,21 @@ +package game.logic.actionsystem; + +import game.logic.GameLogic; +import game.state.Entity; + +/** + * A player action executor provides specific instructions how to manipulate the + * game logic. + */ +public class PlayerActionExecutor { + + /** + * Executes the game logic manupulation. + * + * @param logic The game logic + * @param args Arguments + */ + public void execute(GameLogic logic, Entity... args) { + // TODO + } +} diff --git a/src/main/java/game/state/Entity.java b/src/main/java/game/state/Entity.java index 743fe80..121fae7 100644 --- a/src/main/java/game/state/Entity.java +++ b/src/main/java/game/state/Entity.java @@ -4,24 +4,42 @@ import java.util.Arrays; 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. + * + * They can represent objects, creatures, the player(s), locations, and more. + * Each entity can also contain zero or more other entities, working as an + * abstract container. It can be connected to other entities through keywords, + * e.g. north, south, west, east. A connection can also be associated with + * another entity to model doors or other types of portals. Finally, an entity + * can store generic attributes. + */ public class Entity { public record EntityConnection(Entity to, Entity associatedEntity) { - } private final String id; private final Set attributes = new HashSet<>(); private Entity location; private boolean closed = false; - private final EntitySet contents = new EntitySet(); + private final EntitySet contents; private final Map connections = new HashMap<>(); + private final Map> playerActions = new HashMap<>(); + final Set containingPersistentSets = new HashSet<>(); public Entity(String id, String... attributes) { this.id = id; this.attributes.addAll(Arrays.asList(attributes)); + this.contents = EntitySet.createPersistent(this.id + "::contents"); } public String getId() { @@ -94,7 +112,8 @@ public class Entity { this.connections.put(directionId, new EntityConnection(to, associatedEntity)); } - public void connectBidirectional(String dirIdFromThisToOther, Entity associatedEntity, String dirIdFromOtherToThis, Entity to) { + public void connectBidirectional(String dirIdFromThisToOther, Entity associatedEntity, String dirIdFromOtherToThis, + Entity to) { this.connections.put(dirIdFromThisToOther, new EntityConnection(to, associatedEntity)); to.connections.put(dirIdFromOtherToThis, new EntityConnection(this, associatedEntity)); } @@ -118,6 +137,60 @@ 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 efe6eca..2c831c5 100644 --- a/src/main/java/game/state/EntitySet.java +++ b/src/main/java/game/state/EntitySet.java @@ -3,21 +3,45 @@ 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 { - private final Set entities; - - public EntitySet(Entity... entities) { - this(Arrays.asList(entities)); + public static EntitySet createTemporary(Entity... entities) { + return new EntitySet(null, Arrays.asList(entities)); } - public EntitySet(Collection 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 Set entities; + private final Map> playerActions = new HashMap<>(); + + private EntitySet(String name, Collection entities) { + this.name = name; this.entities = new HashSet<>(entities); + if (this.name != null) { + this.entities.forEach(e -> e.containingPersistentSets.add(this)); + } } public boolean isEmpty() { @@ -28,12 +52,28 @@ public class EntitySet { return Collections.unmodifiableSet(this.entities); } + public int getSize() { + return this.entities.size(); + } + public boolean add(Entity entity) { - return this.entities.add(entity); + if (this.entities.add(entity)) { + if (this.name != null) { + entity.containingPersistentSets.add(this); + } + return true; + } + return false; } public boolean remove(Entity entity) { - return this.entities.remove(entity); + if(this.entities.remove(entity)) { + if(this.name != null) { + entity.containingPersistentSets.remove(this); + } + return true; + } + return false; } public boolean contains(Entity entity) { @@ -41,7 +81,7 @@ public class EntitySet { } public Entity collapse(GameLogic logic) { - if(this.entities.size() <= 1) { + if (this.entities.size() <= 1) { return this.entities.stream().findAny().orElse(null); } else { // TODO if more than 1 candidate, ask user to specify @@ -50,6 +90,22 @@ public class EntitySet { } public EntitySet getFiltered(Predicate acceptFunction) { - return new EntitySet(this.entities.stream().filter(acceptFunction).toList()); + 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 44244ff..0c46864 100644 --- a/src/main/java/game/state/GameState.java +++ b/src/main/java/game/state/GameState.java @@ -19,7 +19,7 @@ public class GameState { } public EntitySet searchForEntity(EntityDescription description) { - return new EntitySet(this.entities.stream().filter(e -> e.getId().equals(description.getMainWord()) + return EntitySet.createTemporary(this.entities.stream().filter(e -> e.getId().equals(description.getMainWord()) && e.getAttributes().containsAll(description.getAttributes())).toList()); } } \ No newline at end of file