Average Density: 0.06
  1 package nl.tudelft.jpacman.level;
  2 
  3 import java.util.ArrayList;
  4 import java.util.HashMap;
  5 import java.util.HashSet;
  6 import java.util.List;
  7 import java.util.Map;
  8 import java.util.Map.Entry;
  9 import java.util.Set;
 10 import java.util.concurrent.Executors;
 11 import java.util.concurrent.ScheduledExecutorService;
 12 import java.util.concurrent.TimeUnit;
 13 
 14 import nl.tudelft.jpacman.board.Board;
 15 import nl.tudelft.jpacman.board.Direction;
 16 import nl.tudelft.jpacman.board.Square;
 17 import nl.tudelft.jpacman.board.Unit;
 18 import nl.tudelft.jpacman.npc.Ghost;
 19 
 20 /**
 21  * A level of Pac-Man. A level consists of the board with the players and the
 22  * AIs on it.
 23  *
 24  * @author Jeroen Roosen 
 25  */
 26 @SuppressWarnings("PMD.TooManyMethods")
 27 public class Level {
 28 
 29     /**
 30      * The board of this level.
 31      */
 32     private final Board board;
 33 
 34     /**
 35      * The lock that ensures moves are executed sequential.
 36      */
 37     private final Object moveLock = new Object();
 38 
 39     /**
 40      * The lock that ensures starting and stopping can't interfere with each
 41      * other.
 42      */
 43     private final Object startStopLock = new Object();
 44 
 45     /**
 46      * The NPCs of this level and, if they are running, their schedules.
 47      */
 48     private final Map<Ghost, ScheduledExecutorService> npcs;
 49 
 50     /**
 51      * <code>true</code> iff this level is currently in progress, i.e. players
 52      * and NPCs can move.
 53      */
 54     private boolean inProgress;
 55 
 56     /**
 57      * The squares from which players can start this game.
 58      */
 59     private final List<Square> startSquares;
 60 
 61     /**
 62      * The start current selected starting square.
 63      */
 64     private int startSquareIndex;
 65 
 66     /**
 67      * The players on this level.
 68      */
 69     private final List<Player> players;
 70 
 71     /**
 72      * The table of possible collisions between units.
 73      */
 74     private final CollisionMap collisions;
 75 
 76     /**
 77      * The objects observing this level.
 78      */
 79     private final Set<LevelObserver> observers;
 80 
 81     /**
 82      * Creates a new level for the board.
 83      *
 84      * @param board
 85      *            The board for the level.
 86      * @param ghosts
 87      *            The ghosts on the board.
 88      * @param startPositions
 89      *            The squares on which players start on this board.
 90      * @param collisionMap
 91      *            The collection of collisions that should be handled.
 92      */
 93     public Level(Board board, List<Ghost> ghosts, List<Square> startPositions,
 94                  CollisionMap collisionMap) {
 95         assert board != null;
 96         assert ghosts != null;
 97         assert startPositions != null;
 98 
 99         this.board = board;
100         this.inProgress = false;
101         this.npcs = new HashMap<>();
102         for (Ghost ghost : ghosts) {
103             npcs.put(ghost, null);
104         }
105         this.startSquares = startPositions;
106         this.startSquareIndex = 0;
107         this.players = new ArrayList<>();
108         this.collisions = collisionMap;
109         this.observers = new HashSet<>();
110     }
111 
112     /**
113      * Adds an observer that will be notified when the level is won or lost.
114      *
115      * @param observer
116      *            The observer that will be notified.
117      */
118     public void addObserver(LevelObserver observer) {
119         observers.add(observer);
120     }
121 
122     /**
123      * Removes an observer if it was listed.
124      *
125      * @param observer
126      *            The observer to be removed.
127      */
128     public void removeObserver(LevelObserver observer) {
129         observers.remove(observer);
130     }
131 
132     /**
133      * Registers a player on this level, assigning him to a starting position. A
134      * player can only be registered once, registering a player again will have
135      * no effect.
136      *
137      * @param player
138      *            The player to register.
139      */
140     public void registerPlayer(Player player) {
141         assert player != null;
142         assert !startSquares.isEmpty();
143 
144         if (players.contains(player)) {
145             return;
146         }
147         players.add(player);
148         Square square = startSquares.get(startSquareIndex);
149         player.occupy(square);
150         startSquareIndex++;
151         startSquareIndex %= startSquares.size();
152     }
153 
154     /**
155      * Returns the board of this level.
156      *
157      * @return The board of this level.
158      */
159     public Board getBoard() {
160         return board;
161     }
162 
163     /**
164      * Moves the unit into the given direction if possible and handles all
165      * collisions.
166      *
167      * @param unit
168      *            The unit to move.
169      * @param direction
170      *            The direction to move the unit in.
171      */
172     public void move(Unit unit, Direction direction) {
173         assert unit != null;
174         assert direction != null;
175         assert unit.hasSquare();
176 
177         if (!isInProgress()) {
178             return;
179         }
180 
181         synchronized (moveLock) {
182             unit.setDirection(direction);
183             Square location = unit.getSquare();
184             Square destination = location.getSquareAt(direction);
185 
186             if (destination.isAccessibleTo(unit)) {
187                 List<Unit> occupants = destination.getOccupants();
188                 unit.occupy(destination);
189                 for (Unit occupant : occupants) {
190                     collisions.collide(unit, occupant);
191                 }
192             }
193             updateObservers();
194         }
195     }
196 
197     /**
198      * Starts or resumes this level, allowing movement and (re)starting the
199      * NPCs.
200      */
201     public void start() {
202         synchronized (startStopLock) {
203             if (isInProgress()) {
204                 return;
205             }
206             startNPCs();
207             inProgress = true;
208             updateObservers();
209         }
210     }
211 
212     /**
213      * Stops or pauses this level, no longer allowing any movement on the board
214      * and stopping all NPCs.
215      */
216     public void stop() {
217         synchronized (startStopLock) {
218             if (!isInProgress()) {
219                 return;
220             }
221             stopNPCs();
222             inProgress = false;
223         }
224     }
225 
226     /**
227      * Starts all NPC movement scheduling.
228      */
229     private void startNPCs() {
230         for (final Ghost npc : npcs.keySet()) {
231             ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
232 
233             service.schedule(new NpcMoveTask(service, npc),
234                 npc.getInterval() / 2, TimeUnit.MILLISECONDS);
235 
236             npcs.put(npc, service);
237         }
238     }
239 
240     /**
241      * Stops all NPC movement scheduling and interrupts any movements being
242      * executed.
243      */
244     private void stopNPCs() {
245         for (Entry<Ghost, ScheduledExecutorService> entry : npcs.entrySet()) {
246             ScheduledExecutorService schedule = entry.getValue();
247             assert schedule != null;
248             schedule.shutdownNow();
249         }
250     }
251 
252     /**
253      * Returns whether this level is in progress, i.e. whether moves can be made
254      * on the board.
255      *
256      * @return <code>true</code> iff this level is in progress.
257      */
258     public boolean isInProgress() {
259         return inProgress;
260     }
261 
262     /**
263      * Updates the observers about the state of this level.
264      */
265     private void updateObservers() {
266         if (!isAnyPlayerAlive()) {
267             for (LevelObserver observer : observers) {
268                 observer.levelLost();
269             }
270         }
271         if (remainingPellets() == 0) {
272             for (LevelObserver observer : observers) {
273                 observer.levelWon();
274             }
275         }
276     }
277 
278     /**
279      * Returns <code>true</code> iff at least one of the players in this level
280      * is alive.
281      *
282      * @return <code>true</code> if at least one of the registered players is
283      *         alive.
284      */
285     public boolean isAnyPlayerAlive() {
286         for (Player player : players) {
287             if (player.isAlive()) {
288                 return true;
289             }
290         }
291         return false;
292     }
293 
294     /**
295      * Counts the pellets remaining on the board.
296      *
297      * @return The amount of pellets remaining on the board.
298      */
299     public int remainingPellets() {
300         Board board = getBoard();
301         int pellets = 0;
302         for (int x = 0; x < board.getWidth(); x++) {
303             for (int y = 0; y < board.getHeight(); y++) {
304                 for (Unit unit : board.squareAt(x, y).getOccupants()) {
305                     if (unit instanceof Pellet) {
306                         pellets++;
307                     }
308                 }
309             }
310         }
311         assert pellets >= 0;
312         return pellets;
313     }
314 
315     /**
316      * A task that moves an NPC and reschedules itself after it finished.
317      *
318      * @author Jeroen Roosen
319      */
320     private final class NpcMoveTask implements Runnable {
321 
322         /**
323          * The service executing the task.
324          */
325         private final ScheduledExecutorService service;
326 
327         /**
328          * The NPC to move.
329          */
330         private final Ghost npc;
331 
332         /**
333          * Creates a new task.
334          *
335          * @param service
336          *            The service that executes the task.
337          * @param npc
338          *            The NPC to move.
339          */
340         NpcMoveTask(ScheduledExecutorService service, Ghost npc) {
341             this.service = service;
342             this.npc = npc;
343         }
344 
345         @Override
346         public void run() {
347             Direction nextMove = npc.nextMove();
348             if (nextMove != null) {
349                 move(npc, nextMove);
350             }
351             long interval = npc.getInterval();
352             service.schedule(this, interval, TimeUnit.MILLISECONDS);
353         }
354     }
355 
356     /**
357      * An observer that will be notified when the level is won or lost.
358      *
359      * @author Jeroen Roosen
360      */
361     public interface LevelObserver {
362 
363         /**
364          * The level has been won. Typically the level should be stopped when
365          * this event is received.
366          */
367         void levelWon();
368 
369         /**
370          * The level has been lost. Typically the level should be stopped when
371          * this event is received.
372          */
373         void levelLost();
374     }
375 }