package inf101.v18.rogue101.game; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.function.Supplier; import inf101.v18.gfx.Screen; import inf101.v18.gfx.gfxmode.ITurtle; import inf101.v18.gfx.gfxmode.TurtlePainter; import inf101.v18.gfx.textmode.Printer; import inf101.v18.grid.GridDirection; import inf101.v18.grid.IGrid; import inf101.v18.grid.ILocation; import inf101.v18.rogue101.Main; import inf101.v18.rogue101.enemies.Girl; import inf101.v18.rogue101.examples.Carrot; import inf101.v18.rogue101.items.IBuffItem; import inf101.v18.rogue101.items.IWeapon; import inf101.v18.rogue101.items.Manga; import inf101.v18.rogue101.examples.Rabbit; import inf101.v18.rogue101.items.Sword; import inf101.v18.rogue101.map.GameMap; import inf101.v18.rogue101.map.IGameMap; import inf101.v18.rogue101.map.IMapView; import inf101.v18.rogue101.map.MapReader; import inf101.v18.rogue101.objects.*; import javafx.scene.canvas.GraphicsContext; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; public class Game implements IGame { /** * All the IActors that have things left to do this turn */ private List actors = Collections.synchronizedList(new ArrayList<>()); /** * For fancy solution to factory problem */ private Map> itemFactories = new HashMap<>(); /** * Useful random generator */ private Random random = new Random(); /** * Saves the last three messages */ private List lastMessages = new ArrayList<>(); /** * The game map. {@link IGameMap} gives us a few more details than * {@link IMapView} (write access to item lists); the game needs this but * individual items don't. */ private IGameMap map; private IActor currentActor; private ILocation currentLocation; private int movePoints = 0; private final ITurtle painter; private final Printer printer; private int numPlayers = 0; public Game(Screen screen, ITurtle painter, Printer printer) { this.painter = painter; this.printer = printer; addFactory(); // NOTE: in a more realistic situation, we will have multiple levels (one map // per level), and (at least for a Roguelike game) the levels should be // generated // // inputGrid will be filled with single-character strings indicating what (if // anything) // should be placed at that map square IGrid inputGrid = MapReader.readFile("maps/testmap.txt"); if (inputGrid == null) { System.err.println("Map not found – falling back to builtin map"); inputGrid = MapReader.readString(Main.BUILTIN_MAP); } this.map = new GameMap(inputGrid.getArea()); for (ILocation loc : inputGrid.locations()) { IItem item = createItem(inputGrid.get(loc)); if (item != null) { map.add(loc, item); } } } public Game(String mapString) { printer = new Printer(1280, 720); painter = new TurtlePainter(1280, 720); addFactory(); IGrid inputGrid = MapReader.readString(mapString); this.map = new GameMap(inputGrid.getArea()); for (ILocation loc : inputGrid.locations()) { IItem item = createItem(inputGrid.get(loc)); if (item != null) { map.add(loc, item); } } } @Override public void addItem(IItem item) { map.add(currentLocation, item); } @Override public void addItem(String sym) { IItem item = createItem(sym); if (item != null) map.add(currentLocation, item); } /** * Calculates the attack of an IActor based on equipped items. * * @return The attack */ private int getAttack() { int damage = currentActor.getAttack() + random.nextInt(20) + 1; IWeapon weapon = (IWeapon)currentActor.getItem(IWeapon.class); if (weapon != null) { damage += weapon.getWeaponDamage(); } IBuffItem buff = (IBuffItem)currentActor.getItem(IBuffItem.class); if (buff != null) { damage += buff.getBuffDamage(); } return damage; } /** * Gets the defence of the current target. * * @param target The target to evaluate * @return The defence of the target */ private int getDefence(IItem target) { int defence = target.getDefence() + 10; IActor actor = (IActor) target; IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class); if (item != null) { defence += item.getBuffDefence(); } return defence; } /** * Gets the damage done against the current target. * * @param target The target to evaluate. * @return The damage done to the target. */ private int getDamage(IItem target) { int damage = currentActor.getDamage(); IActor actor = (IActor) target; IBuffItem item = (IBuffItem) actor.getItem(IBuffItem.class); if (item != null) { damage -= item.getBuffDamageReduction(); } return damage; } @Override public ILocation attack(GridDirection dir, IItem target) { ILocation loc = currentLocation.go(dir); if (!map.has(loc, target)) { throw new IllegalMoveException("Target isn't there!"); } if (getAttack() >= getDefence(target)) { int actualDamage = target.handleDamage(this, target, getDamage(target)); formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); } else { formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); } map.clean(loc); if (target.isDestroyed()) { return move(dir); } else { movePoints--; return currentLocation; } } @Override public ILocation rangedAttack(GridDirection dir, IItem target) { ILocation loc = currentLocation; if (getAttack() >= getDefence(target)) { int damage = getDamage(target) / loc.gridDistanceTo(map.getLocation(target)); int actualDamage = target.handleDamage(this, target, damage); formatMessage("%s hits %s for %d damage", currentActor.getName(), target.getName(), actualDamage); } else { formatMessage("%s tried to hit %s, but missed", currentActor.getName(), target.getName()); } map.clean(map.getLocation(target)); if (target.isDestroyed() && map.has(currentLocation.go(dir), target)) { return move(dir); } else { return currentLocation; } } /** * Begin a new game turn, or continue to the previous turn * * @return True if the game should wait for more user input */ public boolean doTurn() { do { if (actors.isEmpty()) { // System.err.println("new turn!"); // no one in the queue, we're starting a new turn! // first collect all the actors: beginTurn(); } if (random.nextInt(100) < 20) { ILocation loc = map.getLocation(random.nextInt(map.getWidth()), random.nextInt(map.getHeight())); if (!map.hasActors(loc) && !map.hasItems(loc) && !map.hasWall(loc)) { map.add(loc, new Carrot()); } } // process actors one by one; for the IPlayer, we return and wait for keypresses // Possible for INonPlayer, we could also return early (returning // *false*), and then insert a little timer delay between each non-player move // (the timer // is already set up in Main) while (!actors.isEmpty()) { // get the next player or non-player in the queue currentActor = actors.remove(0); if (currentActor.isDestroyed()) // skip if it's dead continue; currentLocation = map.getLocation(currentActor); if (currentLocation == null) { displayDebug("doTurn(): Whoops! Actor has disappeared from the map: " + currentActor); } movePoints = 1; // everyone gets to do one thing if (currentActor instanceof INonPlayer) { // computer-controlled players do their stuff right away ((INonPlayer) currentActor).doTurn(this); // remove any dead items from current location map.clean(currentLocation); return false; } else if (currentActor instanceof IPlayer) { if (currentActor.isDestroyed()) { // a dead human player gets removed from the game // TODO: you might want to be more clever here displayMessage("YOU DIE!!!"); map.remove(currentLocation, currentActor); currentActor = null; currentLocation = null; } else { // For the human player, we need to wait for input, so we just return. // Further keypresses will cause keyPressed() to be called, and once the human // makes a move, it'll lose its movement point and doTurn() will be called again // // NOTE: currentActor and currentLocation are set to the IPlayer (above), // so the game remembers who the player is whenever new keypresses occur. This // is also how e.g., getLocalItems() work – the game always keeps track of // whose turn it is. return true; } } else { displayDebug("doTurn(): Hmm, this is a very strange actor: " + currentActor); } } } while (numPlayers > 0); // we can safely repeat if we have players, since we'll return (and break out of // the loop) once we hit the player return true; } /** * Go through the map and collect all the actors. */ private void beginTurn() { numPlayers = 0; // this extra fancy iteration over each map location runs *in parallel* on // multicore systems! // that makes some things more tricky, hence the "synchronized" block and // "Collections.synchronizedList()" in the initialization of "actors". // NOTE: If you want to modify this yourself, it might be a good idea to replace // "parallelStream()" by "stream()", because weird things can happen when many // things happen // at the same time! (or do INF214 or DAT103 to learn about locks and threading) map.getArea().parallelStream().forEach((loc) -> { // will do this for each location in map List list = map.getAllModifiable(loc); // all items at loc Iterator li = list.iterator(); // manual iterator lets us remove() items while (li.hasNext()) { // this is what "for(IItem item : list)" looks like on the inside IItem item = li.next(); if (item.getCurrentHealth() < 0) { // normally, we expect these things to be removed when they are destroyed, so // this shouldn't happen synchronized (this) { formatDebug("beginTurn(): found and removed leftover destroyed item %s '%s' at %s%n", item.getName(), item.getSymbol(), loc); } li.remove(); map.remove(loc, item); // need to do this too, to update item map } else if (item instanceof IPlayer) { actors.add(0, (IActor) item); // we let the human player go first synchronized (this) { numPlayers++; } } else if (item instanceof IActor) { actors.add((IActor) item); // add other actors to the end of the list } else if (item instanceof Carrot) { ((Carrot) item).doTurn(); } } }); } @Override public boolean canGo(GridDirection dir) { return map.canGo(currentLocation, dir); } private void addFactory() { itemFactories.put("#", Wall::new); itemFactories.put("@", Player::new); itemFactories.put("C", Carrot::new); itemFactories.put("R", Rabbit::new); itemFactories.put("M", Manga::new); itemFactories.put("G", Girl::new); itemFactories.put(".", Dust::new); itemFactories.put("S", Sword::new); } @Override public IItem createItem(String sym) { switch (sym) { case " ": return null; default: // alternative/advanced method Supplier factory = itemFactories.get(sym); if (factory != null) { return factory.get(); } else { System.err.println("createItem: Don't know how to create a '" + sym + "'"); return null; } } } @Override public void displayDebug(String s) { printer.clearLine(Main.LINE_DEBUG); printer.printAt(1, Main.LINE_DEBUG, s, Color.DARKRED); System.err.println(s); } @Override public void displayMessage(String s) { if (lastMessages.size() >= 3) { lastMessages.remove(2); } lastMessages.add(0, s); printer.clearLine(Main.LINE_MSG1); printer.clearLine(Main.LINE_MSG2); printer.clearLine(Main.LINE_MSG3); if (lastMessages.size() > 0) { printer.printAt(1, Main.LINE_MSG1, lastMessages.get(0)); } if (lastMessages.size() > 1) { printer.printAt(1, Main.LINE_MSG2, lastMessages.get(1)); } if (lastMessages.size() > 2) { printer.printAt(1, Main.LINE_MSG3, lastMessages.get(2)); } System.out.println("Message: «" + s + "»"); } @Override public void displayStatus(String s) { printer.clearLine(Main.LINE_STATUS); printer.printAt(1, Main.LINE_STATUS, s); System.out.println("Status: «" + s + "»"); } public void draw() { map.draw(painter, printer); } @Override public boolean drop(IItem item) { if (item != null) { map.add(currentLocation, item); return true; } else return false; } @Override public void formatDebug(String s, Object... args) { displayDebug(String.format(s, args)); } @Override public void formatMessage(String s, Object... args) { displayMessage(String.format(s, args)); } @Override public void formatStatus(String s, Object... args) { displayStatus(String.format(s, args)); } @Override public int getHeight() { return map.getHeight(); } @Override public List getLocalItems() { return map.getItems(currentLocation); } @Override public ILocation getLocation() { return currentLocation; } @Override public ILocation getLocation(GridDirection dir) { if (currentLocation.canGo(dir)) return currentLocation.go(dir); else return null; } /** * Return the game map. {@link IGameMap} gives us a few more details than * {@link IMapView} (write access to item lists); the game needs this but * individual items don't. */ @Override public IMapView getMap() { return map; } @Override public List getPossibleMoves() { List moves = new ArrayList<>(); for (GridDirection dir : GridDirection.FOUR_DIRECTIONS) { if (canGo(dir)) { moves.add(dir); } } return moves; } @Override public List getVisible() { List neighbours = map.getNeighbourhood(currentLocation, currentActor.getVision()); List valid = new ArrayList<>(); for (ILocation neighbour : neighbours) { boolean blocked = false; for (ILocation tile : currentLocation.gridLineTo(neighbour)) { if (map.hasWall(tile)) { blocked = true; break; } } if (!blocked) { valid.add(neighbour); } } return valid; } @Override public int getWidth() { return map.getWidth(); } public boolean keyPressed(KeyCode code) { // only an IPlayer/human can handle keypresses, and only if it's the human's // turn if (currentActor instanceof IPlayer) { return !((IPlayer) currentActor).keyPressed(this, code); } return true; } @Override public ILocation move(GridDirection dir) { if (movePoints < 1) throw new IllegalMoveException("You're out of moves!"); ILocation newLoc = map.go(currentLocation, dir); map.remove(currentLocation, currentActor); map.add(newLoc, currentActor); currentLocation = newLoc; movePoints--; return currentLocation; } @Override public IItem pickUp(IItem item) { if (item != null && map.has(currentLocation, item)) { if (item instanceof IActor) { if (item.getCurrentHealth() / item.getMaxHealth() < 3) { map.remove(currentLocation, item); return item; } else { return null; } } else if (currentActor.getAttack() > item.getDefence()) { map.remove(currentLocation, item); return item; } else { return null; } } else { return null; } } @Override public ITurtle getPainter() { return painter; } @Override public Printer getPrinter() { return printer; } @Override public int[] getFreeTextAreaBounds() { int[] area = new int[4]; area[0] = getWidth() + 1; area[1] = 1; area[2] = printer.getLineWidth(); area[3] = printer.getPageHeight() - 5; return area; } @Override public void clearFreeTextArea() { printer.clearRegion(getWidth() + 1, 1, printer.getLineWidth() - getWidth(), printer.getPageHeight() - 5); } @Override public void clearFreeGraphicsArea() { painter.as(GraphicsContext.class).clearRect(getWidth() * printer.getCharWidth(), 0, painter.getWidth() - getWidth() * printer.getCharWidth(), (printer.getPageHeight() - 5) * printer.getCharHeight()); } @Override public double[] getFreeGraphicsAreaBounds() { double[] area = new double[4]; area[0] = getWidth() * printer.getCharWidth(); area[1] = 0; area[2] = painter.getWidth(); area[3] = getHeight() * printer.getCharHeight(); return area; } @Override public IActor getActor() { return currentActor; } public ILocation setCurrent(IActor actor) { currentLocation = map.getLocation(actor); if (currentLocation != null) { currentActor = actor; movePoints = 1; } return currentLocation; } public IActor setCurrent(ILocation loc) { List list = map.getActors(loc); if (!list.isEmpty()) { currentActor = list.get(0); currentLocation = loc; movePoints = 1; } return currentActor; } public IActor setCurrent(int x, int y) { return setCurrent(map.getLocation(x, y)); } @Override public Random getRandom() { return random; } @Override public GridDirection locationDirection(ILocation start, ILocation target) { int targetX = target.getX(), targetY = target.getY(); int startX = start.getX(), startY = start.getY(); GridDirection dir = GridDirection.CENTER; if (targetX > startX && targetY > startY) { if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { dir = GridDirection.SOUTH; } else { dir = GridDirection.EAST; } } else if (targetX > startX && targetY < startY) { if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { dir = GridDirection.NORTH; } else { dir = GridDirection.EAST; } } else if (targetX < startX && targetY > startY) { if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { dir = GridDirection.SOUTH; } else { dir = GridDirection.WEST; } } else if (targetX < startX && targetY < startY) { if (Math.abs(targetX - startX) < Math.abs(targetY - startY)) { dir = GridDirection.NORTH; } else { dir = GridDirection.WEST; } } else if (targetX > startX) { dir = GridDirection.EAST; } else if (targetX < startX) { dir = GridDirection.WEST; } else if (targetY > startY) { dir = GridDirection.SOUTH; } else if (targetY < startY) { dir = GridDirection.NORTH; } return dir; } }