diff --git a/.gitignore b/.gitignore index c507849..83fe80e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target .idea +.vscode diff --git a/pom.xml b/pom.xml index aabf3d2..9bdeaae 100644 --- a/pom.xml +++ b/pom.xml @@ -3,6 +3,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + 17 + 17 + schmaeddes untitledTextAdventure diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 19c0a36..f63178c 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,30 +1,24 @@ +import java.io.IOException; + +import game.logic.GameLogic; +import game.logic.Parser; +import game.state.CircularLocationException; import startup.Environment; import startup.LoadStuff; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Scanner; - public class Main { - - public static Environment environment = new Environment(); - public static Parser parser = new Parser(); - - public static void main(String[] args) { + public static void main(String[] args) throws IOException { LoadStuff loadStuff = new LoadStuff(); - loadStuff.load(environment); + loadStuff.load(Environment.instance); - String greenPrompt = TextColors.BLUE.colorize(">"); - Scanner scanner = new Scanner(System.in); - - while(true) { - System.out.printf("%s ", greenPrompt); - List input = Arrays.stream(scanner.nextLine().split(" ")).map(o -> o.toLowerCase(Locale.ROOT)).toList(); - - parser.parse(input); + Parser parser = new Parser(); + try (GameLogic logic = new GameLogic(parser)) { + logic.loadGameState("games/damnCoolTextAdventureFTW.json"); + logic.mainLoop(); + } catch (CircularLocationException ex) { + System.err.println("You messed up you game state: " + ex.getMessage()); + ex.printStackTrace(); } - } } diff --git a/src/main/java/Parser.java b/src/main/java/Parser.java deleted file mode 100644 index 707eee7..0000000 --- a/src/main/java/Parser.java +++ /dev/null @@ -1,14 +0,0 @@ -import java.util.List; - -public class Parser { - - public void parse(List parameter) { - String command = parameter.get(0); - - switch (command) { - case "go" -> Commands.go(Main.environment.getAreaByString(parameter.get(1))); - case "info" -> Commands.info(); - } - - } -} diff --git a/src/main/java/game/logic/EntityDescription.java b/src/main/java/game/logic/EntityDescription.java new file mode 100644 index 0000000..46e98dd --- /dev/null +++ b/src/main/java/game/logic/EntityDescription.java @@ -0,0 +1,27 @@ +package game.logic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class EntityDescription { + private final List words; + + public EntityDescription(List words) { + this.words = new ArrayList<>(words); + } + + public String getMainWord() { + // TODO enities made up from multiple words. E.g., flat earth conspiracy + return this.words.get(this.words.size() - 1); + } + + public List getAttributes() { + return Collections.unmodifiableList(this.words.subList(0, this.words.size() - 1)); + } + + @Override + public String toString() { + return String.join(" ", this.words); + } +} diff --git a/src/main/java/game/logic/GameLogic.java b/src/main/java/game/logic/GameLogic.java new file mode 100644 index 0000000..67e3837 --- /dev/null +++ b/src/main/java/game/logic/GameLogic.java @@ -0,0 +1,169 @@ +package game.logic; + +import java.io.Closeable; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Predicate; + +import game.logic.actionsystem.Action; +import game.logic.actionsystem.actions.GoDirection; +import game.logic.actionsystem.actions.Open; +import game.logic.actionsystem.actions.TakeFrom; +import game.state.CircularLocationException; +import game.state.Entity; +import game.state.EntitySet; +import game.state.GameState; + +public class GameLogic implements Closeable { + private final Parser parser; + private GameState state; + private Entity player; + private boolean discontinue = false; + + 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 + //////////////////////////////////////////////////////////////////////// + + // final String NORTH = "north"; + // final String SOUTH = "south"; + final String EAST = "east"; + final String WEST = "west"; + final String INSIDE = "inside"; + final String OUTSIDE = "outside"; + + 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"); + houseMainDoor.setClosed(true); + + forestPath01.connectBidirectional(EAST, null, WEST, clearing); + forestPath01.connectBidirectional(WEST, null, EAST, houseOutside); + houseOutside.connectBidirectional(INSIDE, houseMainDoor, OUTSIDE, houseInside); + + this.player = this.state.createEntity("player"); + Entity apple01 = this.state.createEntity("apple_01", "green"); + Entity apple02 = this.state.createEntity("apple_02", "red"); + + this.player.setLocation(clearing); + apple01.setLocation(forestPath01); + apple02.setLocation(forestPath01); + + //////////////////////////////////////////////////////////////////////// + // TODO game specific action parsers + //////////////////////////////////////////////////////////////////////// + + this.parser.pushActionParser(userInput -> userInput.get(0).equals("go") + ? new GoDirection(userInput.size() < 2 ? null : userInput.get(1)) + : null); + this.parser.pushActionParser(userInput -> { + if (userInput.get(0).equals("take")) { + int fromIdx = userInput.indexOf("from"); + List whatToTake = this + .parseDescriptionList(userInput.subList(1, fromIdx == -1 ? userInput.size() : fromIdx)); + List whereToTakeFrom = fromIdx == -1 ? null + : this.parseDescriptionList(userInput.subList(fromIdx + 1, userInput.size())); + return new TakeFrom(whatToTake, whereToTakeFrom); + } else { + return null; + } + }); + this.parser.pushActionParser(userInput -> { + if (userInput.get(0).equals("open")) { + return new Open(this.parseDescriptionList(userInput.subList(1, userInput.size()))); + } else { + return null; + } + }); + } + + public Entity getPlayer() { + return this.player; + } + + public EntitySet searchForEntity(EntityDescription description) { + return this.state.searchForEntity(description); + } + + public EntitySet searchForEntity(EntityDescription description, Predicate acceptFunction) { + return this.state.searchForEntity(description).getFiltered(acceptFunction); + } + + public EntitySet searchForNearbyEntity(EntityDescription description) { + return this.searchForEntity(description, e -> { + if (this.player == null) { + return false; + } else if (this.player.contains(e, false)) { + return true; + } else if (this.player.getLocation() != null && this.player.getLocation().contains(e, false)) { + return true; + } else { + for (Entity.EntityConnection c : this.player.getLocation().getConnections().stream() + .map(this.player.getLocation()::getConnection).toList()) { + if (c.associatedEntity() == e) { + return true; + } + } + } + return false; + }); + } + + public void mainLoop() { + while (!this.discontinue) { + Action action = this.parser.readAction(); + if (action != null) { + action.execute(this); + } + } + } + + private List parseDescriptionList(List words) { + List descriptions = new LinkedList<>(); + List desc = new LinkedList<>(); + for (String word : words) { + if (word.equals("and")) { + descriptions.add(new EntityDescription(desc)); + desc = new LinkedList<>(); + } else { + desc.add(word); + } + } + if (!desc.isEmpty()) { + descriptions.add(new EntityDescription(desc)); + } + return descriptions; + } + + public void printToUser(String messageId, Object... args) { + System.out.print(messageId); + for (int i = 0; i < args.length; ++i) { + System.out.print(i == 0 ? " " : ", "); + System.out.print(args[i]); + } + System.out.println(); + } + + public boolean canPlayerOpen(Entity entity) { + // TODO + return true; + } + + public boolean canPlayerTake(Entity entity) { + // TODO + return true; + } + + @Override + public void close() throws IOException { + this.parser.close(); + } +} diff --git a/src/main/java/game/logic/Parser.java b/src/main/java/game/logic/Parser.java new file mode 100644 index 0000000..d76f7ac --- /dev/null +++ b/src/main/java/game/logic/Parser.java @@ -0,0 +1,51 @@ +package game.logic; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +import game.logic.actionsystem.Action; +import game.logic.actionsystem.ActionParser; +import startup.Environment; +import util.Commands; +import util.TextColors; + +public class Parser implements Closeable { + private final Scanner scanner = new Scanner(System.in); + private final List actionParsers = new LinkedList<>(); + + public Action readAction() { + String greenPrompt = TextColors.BLUE.colorize(">"); + System.out.printf("%s ", greenPrompt); + List input = Arrays.stream(scanner.nextLine().split("\\s+")).map(String::toLowerCase).toList(); + + for (ActionParser actionParser : this.actionParsers) { + Action action = actionParser.parseAction(input); + if (action != null) { + return action; + } + } + return null; + } + + public void parse(List parameter) { + String command = parameter.get(0); + + switch (command) { + case "go" -> Commands.go(Environment.instance.getAreaByString(parameter.get(1))); + case "info" -> Commands.info(); + } + } + + public void pushActionParser(ActionParser actionParser) { + this.actionParsers.add(actionParser); + } + + @Override + public void close() throws IOException { + this.scanner.close(); + } +} diff --git a/src/main/java/game/logic/actionsystem/Action.java b/src/main/java/game/logic/actionsystem/Action.java new file mode 100644 index 0000000..613496e --- /dev/null +++ b/src/main/java/game/logic/actionsystem/Action.java @@ -0,0 +1,22 @@ +package game.logic.actionsystem; + +import game.logic.GameLogic; + +public abstract class Action { + public enum Type { + TAKE, DROP, + COMBINE_WITH, USE, + GIVE_TO, TAKE_FROM, + PUT_ON, + PUSH, PULL, ROLL, ROLL_TO, + TRACE_RAY_TO, + KILL, KILL_WITH, + SEARCH, SEARCH_FOR, + TALK_TO, TELL_TO, ANNOY, + LOOK_AT, EXAMINE, READ, WRITE_ON_WITH, + HIT, HIT_WITH, + GO_TO, + } + + public abstract void execute(GameLogic logic); +} diff --git a/src/main/java/game/logic/actionsystem/ActionParser.java b/src/main/java/game/logic/actionsystem/ActionParser.java new file mode 100644 index 0000000..d37af4a --- /dev/null +++ b/src/main/java/game/logic/actionsystem/ActionParser.java @@ -0,0 +1,7 @@ +package game.logic.actionsystem; + +import java.util.List; + +public interface ActionParser { + public Action parseAction(List userInput); +} diff --git a/src/main/java/game/logic/actionsystem/actions/GoDirection.java b/src/main/java/game/logic/actionsystem/actions/GoDirection.java new file mode 100644 index 0000000..ce7c217 --- /dev/null +++ b/src/main/java/game/logic/actionsystem/actions/GoDirection.java @@ -0,0 +1,44 @@ +package game.logic.actionsystem.actions; + +import game.logic.GameLogic; +import game.logic.actionsystem.Action; +import game.state.CircularLocationException; +import game.state.Entity; + +public class GoDirection extends Action { + private final String directionId; + + public GoDirection(String directionId) { + this.directionId = directionId; + } + + public String getDirectionId() { + return this.directionId; + } + + @Override + public void execute(GameLogic logic) { + if (logic.getPlayer() == null) { + logic.printToUser("go.player.null"); + } else { + Entity location = logic.getPlayer().getLocation(); + if (location == null) { + logic.printToUser("go.player.location.null", logic.getPlayer()); + } else { + Entity.EntityConnection connection = location.getConnection(this.directionId); + if (connection == null || connection.to() == null) { + logic.printToUser("go.direction.unknown", logic.getPlayer().getLocation(), this.directionId); + } else if (connection.associatedEntity() != null && connection.associatedEntity().isClosed()) { + logic.printToUser("go.location.closed", connection.associatedEntity()); + } else { + try { + logic.getPlayer().setLocation(connection.to()); + logic.printToUser("go.success", location, connection.to()); + } catch (CircularLocationException ex) { + logic.printToUser("go.locationCircular", ex); + } + } + } + } + } +} diff --git a/src/main/java/game/logic/actionsystem/actions/Open.java b/src/main/java/game/logic/actionsystem/actions/Open.java new file mode 100644 index 0000000..b74365d --- /dev/null +++ b/src/main/java/game/logic/actionsystem/actions/Open.java @@ -0,0 +1,32 @@ +package game.logic.actionsystem.actions; + +import java.util.List; + +import game.logic.EntityDescription; +import game.logic.GameLogic; +import game.logic.actionsystem.Action; +import game.state.Entity; +import game.state.EntitySet; + +public class Open extends Action { + private final List descriptions; + + public Open(List descriptions) { + this.descriptions = descriptions; + } + + @Override + public void execute(GameLogic logic) { + for (EntitySet set : this.descriptions.stream().map(logic::searchForNearbyEntity).toList()) { + Entity e = set.collapse(logic); + if(e == null) { + logic.printToUser("open.noEntity"); + } else if (logic.canPlayerOpen(e)) { + e.setClosed(false); + logic.printToUser("open.success", e); + } else { + logic.printToUser("open.failed", e); + } + } + } +} diff --git a/src/main/java/game/logic/actionsystem/actions/TakeFrom.java b/src/main/java/game/logic/actionsystem/actions/TakeFrom.java new file mode 100644 index 0000000..41422e7 --- /dev/null +++ b/src/main/java/game/logic/actionsystem/actions/TakeFrom.java @@ -0,0 +1,50 @@ +package game.logic.actionsystem.actions; + +import java.util.List; + +import game.logic.EntityDescription; +import game.logic.GameLogic; +import game.logic.actionsystem.Action; +import game.state.CircularLocationException; +import game.state.Entity; +import game.state.EntitySet; + +public class TakeFrom extends Action { + private final List what; + private final List fromWhere; + + public TakeFrom(List what, List fromWhere) { + this.what = what; + this.fromWhere = fromWhere; + } + + public List getWhat() { + return this.what; + } + + public List getFromWhere() { + return this.fromWhere; + } + + @Override + public void execute(GameLogic logic) { + // TODO incorporate fromWhere + for (EntityDescription ed : this.what) { + EntitySet es = logic.searchForNearbyEntity(ed); + if(es.isEmpty()) { + logic.printToUser("entity.notFound", ed); + } + Entity e = es.collapse(logic); + if (e != null && logic.canPlayerTake(e)) { + try { + e.setLocation(logic.getPlayer()); + logic.printToUser("take.success", e); + } catch (CircularLocationException ex) { + logic.printToUser("take.error", ex); + } + } else { + logic.printToUser("take.failed", e); + } + } + } +} diff --git a/src/main/java/game/state/CircularLocationException.java b/src/main/java/game/state/CircularLocationException.java new file mode 100644 index 0000000..a40b1cb --- /dev/null +++ b/src/main/java/game/state/CircularLocationException.java @@ -0,0 +1,7 @@ +package game.state; + +public class CircularLocationException extends Exception { + public CircularLocationException(String message) { + super(message); + } +} diff --git a/src/main/java/game/state/Entity.java b/src/main/java/game/state/Entity.java new file mode 100644 index 0000000..743fe80 --- /dev/null +++ b/src/main/java/game/state/Entity.java @@ -0,0 +1,125 @@ +package game.state; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +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 Map connections = new HashMap<>(); + + public Entity(String id, String... attributes) { + this.id = id; + this.attributes.addAll(Arrays.asList(attributes)); + } + + public String getId() { + return this.id; + } + + public Set getAttributes() { + return Collections.unmodifiableSet(this.attributes); + } + + public boolean addAttribute(String attribute) { + return this.attributes.add(attribute); + } + + public boolean removeAttribute(String attribute) { + return this.attributes.remove(attribute); + } + + public void toggleAttribute(String attribute) { + if (!this.attributes.remove(attribute)) { + this.attributes.add(attribute); + } + } + + public void switchAttribute(String attr1, String attr2) { + if (this.attributes.remove(attr1)) { + this.attributes.add(attr2); + } else if (this.attributes.remove(attr2)) { + this.attributes.add(attr1); + } + } + + public void setLocation(Entity location) throws CircularLocationException { + if (location == this) { + throw new CircularLocationException("setLocation.self"); + } else if (this.contains(location, true)) { + throw new CircularLocationException("setLocation.circular"); + } else if (this.location != location) { + if (this.location != null) { + this.location.contents.remove(this); + } + this.location = location; + if (this.location != null) { + this.location.contents.add(this); + } + } + } + + public boolean isClosed() { + return this.closed; + } + + public void setClosed(boolean closed) { + this.closed = closed; + } + + public boolean contains(Entity other, boolean recursive) { + return this.contents.getAll().stream().anyMatch(e -> e == other || (recursive && e.contains(other, true))); + } + + public Entity getLocation() { + return this.location; + } + + public Set getContents() { + return this.contents.getAll(); + } + + public void connectUnidirectional(String directionId, Entity to, Entity associatedEntity) { + this.connections.put(directionId, new EntityConnection(to, associatedEntity)); + } + + 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)); + } + + public void removeSingleConnection(String directionId) { + this.connections.remove(directionId); + } + + public void removeBidirectionalConnection(String dirIdFromThisToOther, String dirIdFromOtherToThis) { + EntityConnection c = this.connections.remove(dirIdFromThisToOther); + if (c != null) { + c.to.connections.remove(dirIdFromOtherToThis); + } + } + + public EntityConnection getConnection(String directionId) { + return this.connections.get(directionId); + } + + public Set getConnections() { + return Collections.unmodifiableSet(this.connections.keySet()); + } + + @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 new file mode 100644 index 0000000..efe6eca --- /dev/null +++ b/src/main/java/game/state/EntitySet.java @@ -0,0 +1,55 @@ +package game.state; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import game.logic.GameLogic; + +public class EntitySet { + private final Set entities; + + public EntitySet(Entity... entities) { + this(Arrays.asList(entities)); + } + + public EntitySet(Collection entities) { + this.entities = new HashSet<>(entities); + } + + public boolean isEmpty() { + return this.entities.isEmpty(); + } + + public Set getAll() { + return Collections.unmodifiableSet(this.entities); + } + + public boolean add(Entity entity) { + return this.entities.add(entity); + } + + public boolean remove(Entity entity) { + return this.entities.remove(entity); + } + + public boolean contains(Entity entity) { + return this.entities.contains(entity); + } + + public Entity collapse(GameLogic logic) { + if(this.entities.size() <= 1) { + return this.entities.stream().findAny().orElse(null); + } else { + // TODO if more than 1 candidate, ask user to specify + return this.entities.stream().findAny().orElse(null); + } + } + + public EntitySet getFiltered(Predicate acceptFunction) { + return new EntitySet(this.entities.stream().filter(acceptFunction).toList()); + } +} diff --git a/src/main/java/game/state/GameState.java b/src/main/java/game/state/GameState.java new file mode 100644 index 0000000..44244ff --- /dev/null +++ b/src/main/java/game/state/GameState.java @@ -0,0 +1,25 @@ +package game.state; + +import java.util.LinkedList; +import java.util.List; + +import game.logic.EntityDescription; + +public class GameState { + private final List entities = new LinkedList<>(); + + public GameState() { + + } + + public Entity createEntity(String id, String... attributes) { + Entity e = new Entity(id, attributes); + this.entities.add(e); + return e; + } + + public EntitySet searchForEntity(EntityDescription description) { + return new EntitySet(this.entities.stream().filter(e -> e.getId().equals(description.getMainWord()) + && e.getAttributes().containsAll(description.getAttributes())).toList()); + } +} \ No newline at end of file diff --git a/src/main/java/startup/Environment.java b/src/main/java/startup/Environment.java index b57aaa0..6713cd3 100644 --- a/src/main/java/startup/Environment.java +++ b/src/main/java/startup/Environment.java @@ -1,10 +1,11 @@ package startup; -import areas.Area; - import java.util.List; +import areas.Area; + public class Environment { + public static final Environment instance = new Environment(); private Area currentArea; private List gameAreas; diff --git a/src/main/java/Commands.java b/src/main/java/util/Commands.java similarity index 55% rename from src/main/java/Commands.java rename to src/main/java/util/Commands.java index 084d9f5..6acfcb9 100644 --- a/src/main/java/Commands.java +++ b/src/main/java/util/Commands.java @@ -1,17 +1,20 @@ +package util; + import areas.Area; +import startup.Environment; public class Commands { public static void go(Area area) { - Main.environment.setArea(area); + Environment.instance.setArea(area); area.enterArea(); } public static void info() { String infoText = TextColors.PURPLE.colorize("Du bist hier: " + - Main.environment.getCurrentArea().getNameWithArticle()) + "\n" + + Environment.instance.getCurrentArea().getNameWithArticle()) + "\n" + "Du kannst folgende Bereiche von hier erreichen: " + - String.join(", ", Main.environment.getCurrentArea().getReachableAreas()); + String.join(", ", Environment.instance.getCurrentArea().getReachableAreas()); System.out.println(infoText); } diff --git a/src/main/java/TextColors.java b/src/main/java/util/TextColors.java similarity index 94% rename from src/main/java/TextColors.java rename to src/main/java/util/TextColors.java index 1b0c5a0..6bc6939 100644 --- a/src/main/java/TextColors.java +++ b/src/main/java/util/TextColors.java @@ -1,5 +1,4 @@ -import org.w3c.dom.Text; - +package util; public enum TextColors { RESET("\u001B[0m"),