From cba582ddc6a77e0d1a2725c3f7ca079e66479888 Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Fri, 6 May 2016 14:55:41 +0200 Subject: Modularize next game state transition --- src/ch/epfl/xblast/server/GameState.java | 373 ++------------------- .../epfl/xblast/server/GameStateTransitioner.java | 349 +++++++++++++++++++ 2 files changed, 372 insertions(+), 350 deletions(-) create mode 100644 src/ch/epfl/xblast/server/GameStateTransitioner.java (limited to 'src') diff --git a/src/ch/epfl/xblast/server/GameState.java b/src/ch/epfl/xblast/server/GameState.java index b304a4d..564ef38 100644 --- a/src/ch/epfl/xblast/server/GameState.java +++ b/src/ch/epfl/xblast/server/GameState.java @@ -6,7 +6,6 @@ import ch.epfl.xblast.*; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; /** @@ -59,329 +58,26 @@ public final class GameState { } /** - * Maps bombs to their owning player ID. - * - * @param b the list of bombs - * @return the mapping - */ - private static Map> mapBombsToPlayers(List b) { - return b.stream().collect(Collectors.groupingBy(Bomb::ownerId)); - } - - /** - * Computes the next state of a blast. - * - * @param blasts0 existing particles - * @param board0 the game's board - * @param explosions0 active explosions - * @return the position of the explosion's particles for the next state. - */ - private static List> nextBlasts(List> blasts0, Board board0, List>> explosions0) { - return Stream.concat( - blasts0.stream() - .filter(b -> board0.blockAt(b.head()).isFree()) - .map(Sq::tail), - explosions0.stream() - .filter(e -> !e.isEmpty()) - .map(Sq::head)) - .filter(b -> !b.isEmpty()) - .collect(Collectors.toList()); - } - - /** - * Computes and returns the next board state of the given board according to the given events. - * - * @param board0 the previous board - * @param consumedBonuses the set of consumed bonuses - * @param blastedCells1 the set of newly blasted cells - * @return the next board - */ - private static Board nextBoard(Board board0, Set consumedBonuses, Set blastedCells1) { - return new Board(Cell.ROW_MAJOR_ORDER.stream() - .map(c -> GameState.nextBlockSeq(c, board0.blocksAt(c), consumedBonuses, blastedCells1)) - .collect(Collectors.toList())); - } - - /** - * Returns the next Block sequence for the given cell according to the current state and given events. - * - * @param c the Cell - * @param bs0 the previous Block sequence - * @param consumedBonuses the bonus consumption event - * @param blastedCells1 the new Cell blast events - * @return the new Block sequence - */ - private static Sq nextBlockSeq(Cell c, Sq bs0, Set consumedBonuses, Set blastedCells1) { - Block b = bs0.head(); - - if (consumedBonuses.contains(c) && b.isBonus()) - return Sq.constant(Block.FREE); - - if (blastedCells1.contains(c)) - if (b == Block.DESTRUCTIBLE_WALL) - return Sq.repeat(Ticks.WALL_CRUMBLING_TICKS, Block.CRUMBLING_WALL) - .concat(Sq.constant(Block.getRandomBonusBlock())); - - else if (b.isBonus()) - return Sq.repeat(Ticks.BONUS_DISAPPEARING_TICKS, b) - .concat(Sq.constant(Block.FREE)); - - return bs0.tail(); - } - - /** - * Computes and returns the next player list given the current one and the given events and states. - * - * @param players0 the previous player list - * @param playerBonuses the map of player bonuses - * @param bombedCells1 the set of newly bombed cells - * @param board1 the newly updated board - * @param blastedCells1 the set of newly blasted cells - * @param speedChangeEvents the speed change events - * @return the next player list - */ - private static List nextPlayers(List players0, Map playerBonuses, - Set bombedCells1, Board board1, Set blastedCells1, - Map> speedChangeEvents) { - - return players0.stream() - .map(p -> GameState.nextPlayer( - p, playerBonuses.get(p.id()), - bombedCells1, board1, blastedCells1, - speedChangeEvents.get(p.id()))) - .collect(Collectors.toList()); - } - - /** - * Computes and returns the next State of a Player from the given events. - * - * @param player0 the Player - * @param playerBonus the optional Bonus to apply to the Player - * @param bombedCells1 the Set of bombed Cells - * @param board1 the updated Board - * @param blastedCells1 the Set of blasted Cells - * @param requestedDirection the Player's new requested Direction - * @return the next state of the Player - */ - private static Player nextPlayer(Player player0, Bonus playerBonus, - Set bombedCells1, Board board1, Set blastedCells1, - Optional requestedDirection) { - - // 1. Compute the new path to follow - Sq updatedPath = GameState.nextPath(player0.directedPositions(), requestedDirection); - - // 2. Follow the path if the Player can (moving life state and no collision) - Sq directedPos1 = GameState.moveAhead(player0, updatedPath, board1, bombedCells1); - - // 3. Apply damages and generate a new LifeState Sequence - Sq lifeStates1 = GameState.nextLifeState(player0, directedPos1, blastedCells1); - - // 4. Create the new player given the new parameters - Player p1 = new Player(player0.id(), lifeStates1, directedPos1, player0.maxBombs(), player0.bombRange()); - - // 5. Update the capacities of the player given the possible bonus - if (!Objects.isNull(playerBonus)) - p1 = playerBonus.applyTo(p1); - - return p1; - } - - /** - * Computes the new path to follow according to the Player's wishes and the Board constraints. - * - * @param p0 the current path - * @param newDir the new requested Direction - * @return the new path - */ - private static Sq nextPath(Sq p0, Optional newDir) { - if (Objects.isNull(newDir)) - return p0; - - if (!newDir.isPresent()) - return GameState.stopPath(p0); - - if (p0.head().direction().isPerpendicularTo(newDir.get())) - return GameState.pivotPath(Player.DirectedPosition.moving(p0.head()), newDir.get()); - - return Player.DirectedPosition.moving(new Player.DirectedPosition(p0.head().position(), newDir.get())); - } - - /** - * Builds and returns a path stopping at the next central SubCell. - * - * @param p0 the current path - * @return the stop path - */ - private static Sq stopPath(Sq p0) { - return GameState - .pathToNextCentralPosition(p0) - .concat(Player.DirectedPosition.stopped(GameState.nextCentral(p0))); - } - - /** - * Builds and returns a pivot path reaching the next pivot SubCell and then rotating to the given Direction. - * - * @param p0 the initial path - * @param newDir the new Direction to follow when possible - * @return the pivot path - */ - private static Sq pivotPath(Sq p0, Direction newDir) { - SubCell nextCentralPos = GameState.nextCentral(p0).position(); - - return GameState - .pathToNextCentralPosition(p0) - .concat(Player.DirectedPosition.moving(new Player.DirectedPosition(nextCentralPos, newDir))); - } - - /** - * Returns the path to the next central SubCell. - * - * @param p the path to follow - * @return the truncated path - */ - private static Sq pathToNextCentralPosition(Sq p) { - return p.takeWhile(dp -> !dp.position().isCentral()); - } - - /** - * Searches for and returns the next central SubCell reachable following the given path. - * - * @param p the path to follow - * @return the next central SubCell - */ - private static Player.DirectedPosition nextCentral(Sq p) { - return p.findFirst(dp -> dp.position().isCentral()); - } - - /** - * Checks for possible collisions and update the path if necessary. - * - * @param path the current path projection - * @param board1 the updated Board - * @param bombedCells1 the Set of bombed Cell-s - * @return the corrected path - */ - private static Sq moveAhead(Player player0, - Sq path, - Board board1, Set bombedCells1) { - - if (!player0.lifeState().canMove()) - return path; - - if (GameState.isColliding(path, board1, bombedCells1)) - return path; - - return path.tail(); - } - - /** - * Returns T(the player is colliding with an object). - * - * @param path the path - * @param board1 the updated Board - * @param bombedCells1 the Set of bombed Cell-s - * @return T(the player is colliding with an object) - */ - private static boolean isColliding(Sq path, Board board1, Set bombedCells1) { - SubCell nextPos = path.tail().head().position(); - SubCell nextCentralPos = GameState.nextCentral(Player.DirectedPosition.moving(path.tail().head())).position(); - - if (!board1.blockAt(nextCentralPos.containingCell()).canHostPlayer()) - return true; - - if (bombedCells1.contains(nextCentralPos.containingCell())) - if (nextPos.distanceTo(nextCentralPos) == Board.BOMB_BLOCKING_DISTANCE - 1) - return true; - - return false; - } - - /** - * Applies damages and generate a new LifeState sequence. - * - * @param player0 the Player - * @param directedPositions1 the DirectedPosition sequence - * @param blastedCells1 the Set of blasted Cell-s - * @return the next LifeState sequence - */ - private static Sq nextLifeState(Player player0, Sq directedPositions1, - Set blastedCells1) { - - Cell currentCell = directedPositions1.head().position().containingCell(); - - if (player0.isVulnerable() && blastedCells1.contains(currentCell)) - return player0.statesForNextLife(); - else - return player0.lifeStates().tail(); - } - - /** - * Computes and returns the next state of the given explosion list. - * - * @param explosions0 the previous explosion state - * @return the next explosion state - */ - private static List>> nextExplosions(List>> explosions0) { - return explosions0.stream() - .map(Sq::tail) - .filter(s -> !s.isEmpty()) - .collect(Collectors.toList()); - } - - /** - * Returns a list of exploding bombs (reached by a blast or consumed fuse). + * Maps the given bombs to their position. * - * @param bombs the list of bombs - * @param blastedCells the set of blasted cells - * @return the list of exploding bombs + * @param bombs a list of bombs + * @return a map of positions and their bombs */ - private static List explodingBombs(List bombs, Set blastedCells) { + static Map bombedCells(List bombs) { return bombs.stream() - .filter(b -> blastedCells.contains(b.position()) || b.fuseLengths().tail().isEmpty()) - .collect(Collectors.toList()); - } - - /** - * Computes the consumption of the bombs. - * - * @param bombs0 the list of bombs - * @param explodingBombs the list of exploding bombs - * @return the new bombs states - */ - private static List nextBombs(List bombs0, List explodingBombs) { - return bombs0.stream() - .filter(b -> !explodingBombs.contains(b)) - .map(b -> new Bomb(b.ownerId(), b.position(), b.fuseLengths().tail(), b.range())) - .collect(Collectors.toList()); + .collect(Collectors.toMap(Bomb::position, Function.identity())); } /** - * Computes and returns the list of newly dropped bombs by players according to the given player states and events. - * Given bomb drop events must be conflict-free. + * Returns a set of cells that contains at least one blast in the given blasts sequences. * - * @param players0 the previous player states - * @param bombDropEvents the bomb drop events - * @param bombs0 the previous bomb state - * @return the newly dropped bombs + * @param blasts the list of blast sequences + * @return the set of blasted cells */ - private static List newlyDroppedBombs(List players0, Set bombDropEvents, List bombs0) { - HashSet bombedCells = new HashSet<>(GameState.bombedCells(bombs0).keySet()); - Map> bombOwnerMap = GameState.mapBombsToPlayers(bombs0); - List bombs1 = new ArrayList<>(bombs0); - - for (Player p : players0) { - if (!bombDropEvents.contains(p.id())) continue; - if (bombedCells.contains(p.position().containingCell())) continue; - - List ownedBombs = bombOwnerMap.get(p.id()); - if (ownedBombs != null && ownedBombs.size() >= p.maxBombs()) continue; - - Bomb b = p.newBomb(); - bombedCells.add(b.position()); - bombs1.add(b); - } - - return bombs1; + static Set blastedCells(List> blasts) { + return blasts.stream() + .map(Sq::head) + .collect(Collectors.toSet()); } /** @@ -458,18 +154,7 @@ public final class GameState { * @return the map of bombs */ public Map bombedCells() { - return GameState.bombedCells(this.bombs); - } - - /** - * Maps the given bombs to their position. - * - * @param bombs a list of bombs - * @return a map of positions and their bombs - */ - private static Map bombedCells(List bombs) { - return bombs.stream() - .collect(Collectors.toMap(Bomb::position, Function.identity())); + return bombedCells(this.bombs); } /** @@ -478,19 +163,7 @@ public final class GameState { * @return the set of blasted cells */ public Set blastedCells() { - return GameState.blastedCells(this.blasts); - } - - /** - * Returns a set of cells that contains at least one blast in the given blasts sequences. - * - * @param blasts the list of blast sequences - * @return the set of blasted cells - */ - private static Set blastedCells(List> blasts) { - return blasts.stream() - .map(Sq::head) - .collect(Collectors.toSet()); + return blastedCells(this.blasts); } /** @@ -502,31 +175,31 @@ public final class GameState { */ public GameState next(Map> speedChangeEvents, Set bombDropEvents) { // 1. blasts evolution - List> blasts1 = GameState.nextBlasts(this.blasts, this.board, this.explosions); + List> blasts1 = GameStateTransitioner.nextBlasts(this.blasts, this.board, this.explosions); // 2. board evolution - Set blastedCells1 = GameState.blastedCells(blasts1); + Set blastedCells1 = blastedCells(blasts1); Map consumedBonuses = this.consumedBonuses(); - Board board1 = GameState.nextBoard(this.board, consumedBonuses.keySet(), blastedCells1); + Board board1 = GameStateTransitioner.nextBoard(this.board, consumedBonuses.keySet(), blastedCells1); // 3. existing explosions evolution - List>> explosions1 = GameState.nextExplosions(this.explosions); + List>> explosions1 = GameStateTransitioner.nextExplosions(this.explosions); // 4.1. newly dropped bombs addition Set topPriorityBombDropEvents = discardConflictingBombDropEvents(bombDropEvents); - List allBombs = GameState.newlyDroppedBombs(this.alivePlayers(), topPriorityBombDropEvents, this.bombs); + List allBombs = GameStateTransitioner.newlyDroppedBombs(this.alivePlayers(), topPriorityBombDropEvents, this.bombs); // 4.2. existing bombs evolution - List explodingBombs = GameState.explodingBombs(allBombs, blastedCells1); - List bombs1 = GameState.nextBombs(allBombs, explodingBombs); + List explodingBombs = GameStateTransitioner.explodingBombs(allBombs, blastedCells1); + List bombs1 = GameStateTransitioner.nextBombs(allBombs, explodingBombs); // 4.3. subsequent explosions addition explodingBombs.forEach(b -> explosions1.addAll(b.explosion())); // 5. players evolution Map playerBonuses = mapPlayersToBonuses(consumedBonuses); - Set bombedCells1 = GameState.bombedCells(bombs1).keySet(); - List players1 = GameState.nextPlayers( + Set bombedCells1 = bombedCells(bombs1).keySet(); + List players1 = GameStateTransitioner.nextPlayers( this.players, playerBonuses, bombedCells1, board1, blastedCells1, speedChangeEvents); return new GameState(this.ticks + 1, board1, players1, bombs1, explosions1, blasts1); diff --git a/src/ch/epfl/xblast/server/GameStateTransitioner.java b/src/ch/epfl/xblast/server/GameStateTransitioner.java new file mode 100644 index 0000000..55d3cda --- /dev/null +++ b/src/ch/epfl/xblast/server/GameStateTransitioner.java @@ -0,0 +1,349 @@ +package ch.epfl.xblast.server; + +import ch.epfl.cs108.Sq; +import ch.epfl.xblast.Cell; +import ch.epfl.xblast.Direction; +import ch.epfl.xblast.PlayerID; +import ch.epfl.xblast.SubCell; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The game state transitioner providing methods for game state evolution. + * + * @author Pacien TRAN-GIRARD (261948) + * @author Timothée FLOURE (257420) + */ +final class GameStateTransitioner { + + private GameStateTransitioner() { + // Static class + } + + /** + * Maps bombs to their owning player ID. + * + * @param b the list of bombs + * @return the mapping + */ + private static Map> mapBombsToPlayers(List b) { + return b.stream().collect(Collectors.groupingBy(Bomb::ownerId)); + } + + /** + * Computes the next state of a blast. + * + * @param blasts0 existing particles + * @param board0 the game's board + * @param explosions0 active explosions + * @return the position of the explosion's particles for the next state. + */ + static List> nextBlasts(List> blasts0, Board board0, List>> explosions0) { + return Stream.concat( + blasts0.stream() + .filter(b -> board0.blockAt(b.head()).isFree()) + .map(Sq::tail), + explosions0.stream() + .filter(e -> !e.isEmpty()) + .map(Sq::head)) + .filter(b -> !b.isEmpty()) + .collect(Collectors.toList()); + } + + /** + * Computes and returns the next board state of the given board according to the given events. + * + * @param board0 the previous board + * @param consumedBonuses the set of consumed bonuses + * @param blastedCells1 the set of newly blasted cells + * @return the next board + */ + static Board nextBoard(Board board0, Set consumedBonuses, Set blastedCells1) { + return new Board(Cell.ROW_MAJOR_ORDER.stream() + .map(c -> nextBlockSeq(c, board0.blocksAt(c), consumedBonuses, blastedCells1)) + .collect(Collectors.toList())); + } + + /** + * Returns the next Block sequence for the given cell according to the current state and given events. + * + * @param c the Cell + * @param bs0 the previous Block sequence + * @param consumedBonuses the bonus consumption event + * @param blastedCells1 the new Cell blast events + * @return the new Block sequence + */ + private static Sq nextBlockSeq(Cell c, Sq bs0, Set consumedBonuses, Set blastedCells1) { + Block b = bs0.head(); + + if (consumedBonuses.contains(c) && b.isBonus()) + return Sq.constant(Block.FREE); + + if (blastedCells1.contains(c)) + if (b == Block.DESTRUCTIBLE_WALL) + return Sq.repeat(Ticks.WALL_CRUMBLING_TICKS, Block.CRUMBLING_WALL) + .concat(Sq.constant(Block.getRandomBonusBlock())); + + else if (b.isBonus()) + return Sq.repeat(Ticks.BONUS_DISAPPEARING_TICKS, b) + .concat(Sq.constant(Block.FREE)); + + return bs0.tail(); + } + + /** + * Computes and returns the next player list given the current one and the given events and states. + * + * @param players0 the previous player list + * @param playerBonuses the map of player bonuses + * @param bombedCells1 the set of newly bombed cells + * @param board1 the newly updated board + * @param blastedCells1 the set of newly blasted cells + * @param speedChangeEvents the speed change events + * @return the next player list + */ + static List nextPlayers(List players0, Map playerBonuses, + Set bombedCells1, Board board1, Set blastedCells1, + Map> speedChangeEvents) { + + return players0.stream() + .map(p -> nextPlayer( + p, playerBonuses.get(p.id()), + bombedCells1, board1, blastedCells1, + speedChangeEvents.get(p.id()))) + .collect(Collectors.toList()); + } + + /** + * Computes and returns the next State of a Player from the given events. + * + * @param player0 the Player + * @param playerBonus the optional Bonus to apply to the Player + * @param bombedCells1 the Set of bombed Cells + * @param board1 the updated Board + * @param blastedCells1 the Set of blasted Cells + * @param requestedDirection the Player's new requested Direction + * @return the next state of the Player + */ + private static Player nextPlayer(Player player0, Bonus playerBonus, + Set bombedCells1, Board board1, Set blastedCells1, + Optional requestedDirection) { + + // 1. Compute the new path to follow + Sq updatedPath = nextPath(player0.directedPositions(), requestedDirection); + + // 2. Follow the path if the Player can (moving life state and no collision) + Sq directedPos1 = moveAhead(player0, updatedPath, board1, bombedCells1); + + // 3. Apply damages and generate a new LifeState Sequence + Sq lifeStates1 = nextLifeState(player0, directedPos1, blastedCells1); + + // 4. Create the new player given the new parameters + Player p1 = new Player(player0.id(), lifeStates1, directedPos1, player0.maxBombs(), player0.bombRange()); + + // 5. Update the capacities of the player given the possible bonus + if (!Objects.isNull(playerBonus)) + p1 = playerBonus.applyTo(p1); + + return p1; + } + + /** + * Computes the new path to follow according to the Player's wishes and the Board constraints. + * + * @param p0 the current path + * @param newDir the new requested Direction + * @return the new path + */ + private static Sq nextPath(Sq p0, Optional newDir) { + if (Objects.isNull(newDir)) + return p0; + + if (!newDir.isPresent()) + return stopPath(p0); + + if (p0.head().direction().isPerpendicularTo(newDir.get())) + return pivotPath(Player.DirectedPosition.moving(p0.head()), newDir.get()); + + return Player.DirectedPosition.moving(new Player.DirectedPosition(p0.head().position(), newDir.get())); + } + + /** + * Builds and returns a path stopping at the next central SubCell. + * + * @param p0 the current path + * @return the stop path + */ + private static Sq stopPath(Sq p0) { + return pathToNextCentralPosition(p0) + .concat(Player.DirectedPosition.stopped(nextCentral(p0))); + } + + /** + * Builds and returns a pivot path reaching the next pivot SubCell and then rotating to the given Direction. + * + * @param p0 the initial path + * @param newDir the new Direction to follow when possible + * @return the pivot path + */ + private static Sq pivotPath(Sq p0, Direction newDir) { + SubCell nextCentralPos = nextCentral(p0).position(); + + return pathToNextCentralPosition(p0) + .concat(Player.DirectedPosition.moving(new Player.DirectedPosition(nextCentralPos, newDir))); + } + + /** + * Returns the path to the next central SubCell. + * + * @param p the path to follow + * @return the truncated path + */ + private static Sq pathToNextCentralPosition(Sq p) { + return p.takeWhile(dp -> !dp.position().isCentral()); + } + + /** + * Searches for and returns the next central SubCell reachable following the given path. + * + * @param p the path to follow + * @return the next central SubCell + */ + private static Player.DirectedPosition nextCentral(Sq p) { + return p.findFirst(dp -> dp.position().isCentral()); + } + + /** + * Checks for possible collisions and update the path if necessary. + * + * @param path the current path projection + * @param board1 the updated Board + * @param bombedCells1 the Set of bombed Cell-s + * @return the corrected path + */ + private static Sq moveAhead(Player player0, + Sq path, + Board board1, Set bombedCells1) { + + if (!player0.lifeState().canMove()) + return path; + + if (isColliding(path, board1, bombedCells1)) + return path; + + return path.tail(); + } + + /** + * Returns T(the player is colliding with an object). + * + * @param path the path + * @param board1 the updated Board + * @param bombedCells1 the Set of bombed Cell-s + * @return T(the player is colliding with an object) + */ + private static boolean isColliding(Sq path, Board board1, Set bombedCells1) { + SubCell nextPos = path.tail().head().position(); + SubCell nextCentralPos = nextCentral(Player.DirectedPosition.moving(path.tail().head())).position(); + + if (!board1.blockAt(nextCentralPos.containingCell()).canHostPlayer()) + return true; + + if (bombedCells1.contains(nextCentralPos.containingCell())) + if (nextPos.distanceTo(nextCentralPos) == Board.BOMB_BLOCKING_DISTANCE - 1) + return true; + + return false; + } + + /** + * Applies damages and generate a new LifeState sequence. + * + * @param player0 the Player + * @param directedPositions1 the DirectedPosition sequence + * @param blastedCells1 the Set of blasted Cell-s + * @return the next LifeState sequence + */ + private static Sq nextLifeState(Player player0, Sq directedPositions1, + Set blastedCells1) { + + Cell currentCell = directedPositions1.head().position().containingCell(); + + if (player0.isVulnerable() && blastedCells1.contains(currentCell)) + return player0.statesForNextLife(); + else + return player0.lifeStates().tail(); + } + + /** + * Computes and returns the next state of the given explosion list. + * + * @param explosions0 the previous explosion state + * @return the next explosion state + */ + static List>> nextExplosions(List>> explosions0) { + return explosions0.stream() + .map(Sq::tail) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); + } + + /** + * Returns a list of exploding bombs (reached by a blast or consumed fuse). + * + * @param bombs the list of bombs + * @param blastedCells the set of blasted cells + * @return the list of exploding bombs + */ + static List explodingBombs(List bombs, Set blastedCells) { + return bombs.stream() + .filter(b -> blastedCells.contains(b.position()) || b.fuseLengths().tail().isEmpty()) + .collect(Collectors.toList()); + } + + /** + * Computes the consumption of the bombs. + * + * @param bombs0 the list of bombs + * @param explodingBombs the list of exploding bombs + * @return the new bombs states + */ + static List nextBombs(List bombs0, List explodingBombs) { + return bombs0.stream() + .filter(b -> !explodingBombs.contains(b)) + .map(b -> new Bomb(b.ownerId(), b.position(), b.fuseLengths().tail(), b.range())) + .collect(Collectors.toList()); + } + + /** + * Computes and returns the list of newly dropped bombs by players according to the given player states and events. + * Given bomb drop events must be conflict-free. + * + * @param players0 the previous player states + * @param bombDropEvents the bomb drop events + * @param bombs0 the previous bomb state + * @return the newly dropped bombs + */ + static List newlyDroppedBombs(List players0, Set bombDropEvents, List bombs0) { + HashSet bombedCells = new HashSet<>(GameState.bombedCells(bombs0).keySet()); + Map> bombOwnerMap = mapBombsToPlayers(bombs0); + List bombs1 = new ArrayList<>(bombs0); + + for (Player p : players0) { + if (!bombDropEvents.contains(p.id())) continue; + if (bombedCells.contains(p.position().containingCell())) continue; + + List ownedBombs = bombOwnerMap.get(p.id()); + if (ownedBombs != null && ownedBombs.size() >= p.maxBombs()) continue; + + Bomb b = p.newBomb(); + bombedCells.add(b.position()); + bombs1.add(b); + } + + return bombs1; + } + +} -- cgit v1.2.3