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 }