1 package nl.tudelft.jpacman.level;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7 import java.util.ArrayList;
8 import java.util.List;
9
10 import nl.tudelft.jpacman.PacmanConfigurationException;
11 import nl.tudelft.jpacman.board.Board;
12 import nl.tudelft.jpacman.board.BoardFactory;
13 import nl.tudelft.jpacman.board.Square;
14 import nl.tudelft.jpacman.npc.Ghost;
15 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16
17 /**
18 * Creates new {@link Level}s from text representations.
19 *
20 * @author Jeroen Roosen
21 */
22 public class MapParser {
23
24 /**
25 * The factory that creates the levels.
26 */
27 private final LevelFactory levelCreator;
28
29 /**
30 * The factory that creates the squares and board.
31 */
32 private final BoardFactory boardCreator;
33
34 /**
35 * Creates a new map parser.
36 *
37 * @param levelFactory
38 * The factory providing the NPC objects and the level.
39 * @param boardFactory
40 * The factory providing the Square objects and the board.
41 */
42 public MapParser(LevelFactory levelFactory, BoardFactory boardFactory) {
43 this.levelCreator = levelFactory;
44 this.boardCreator = boardFactory;
45 }
46
47 /**
48 * Parses the text representation of the board into an actual level.
49 *
50 * <ul>
51 * <li>Supported characters:
52 * <li>' ' (space) an empty square.
53 * <li>'#' (bracket) a wall.
54 * <li>'.' (period) a square with a pellet.
55 * <li>'P' (capital P) a starting square for players.
56 * <li>'G' (capital G) a square with a ghost.
57 * </ul>
58 *
59 * @param map
60 * The text representation of the board, with map[x][y]
61 * representing the square at position x,y.
62 * @return The level as represented by this text.
63 */
64 public Level parseMap(char[][] map) {
65 int width = map.length;
66 int height = map[0].length;
67
68 Square[][] grid = new Square[width][height];
69
70 List<Ghost> ghosts = new ArrayList<>();
71 List<Square> startPositions = new ArrayList<>();
72
73 makeGrid(map, width, height, grid, ghosts, startPositions);
74
75 Board board = boardCreator.createBoard(grid);
76 return levelCreator.createLevel(board, ghosts, startPositions);
77 }
78
79 private void makeGrid(char[][] map, int width, int height,
80 Square[][] grid, List<Ghost> ghosts, List<Square> startPositions) {
81 for (int x = 0; x < width; x++) {
82 for (int y = 0; y < height; y++) {
83 char c = map[x][y];
84 addSquare(grid, ghosts, startPositions, x, y, c);
85 }
86 }
87 }
88
89 /**
90 * Adds a square to the grid based on a given character. These
91 * character come from the map files and describe the type
92 * of square.
93 *
94 * @param grid
95 * The grid of squares with board[x][y] being the
96 * square at column x, row y.
97 * @param ghosts
98 * List of all ghosts that were added to the map.
99 * @param startPositions
100 * List of all start positions that were added
101 * to the map.
102 * @param x
103 * x coordinate of the square.
104 * @param y
105 * y coordinate of the square.
106 * @param c
107 * Character describing the square type.
108 */
109 protected void addSquare(Square[][] grid, List<Ghost> ghosts,
110 List<Square> startPositions, int x, int y, char c) {
111 switch (c) {
112 case ' ':
113 grid[x][y] = boardCreator.createGround();
114 break;
115 case '#':
116 grid[x][y] = boardCreator.createWall();
117 break;
118 case '.':
119 Square pelletSquare = boardCreator.createGround();
120 grid[x][y] = pelletSquare;
121 levelCreator.createPellet().occupy(pelletSquare);
122 break;
123 case 'G':
124 Square ghostSquare = makeGhostSquare(ghosts, levelCreator.createGhost());
125 grid[x][y] = ghostSquare;
126 break;
127 case 'P':
128 Square playerSquare = boardCreator.createGround();
129 grid[x][y] = playerSquare;
130 startPositions.add(playerSquare);
131 break;
132 default:
133 throw new PacmanConfigurationException("Invalid character at "
134 + x + "," + y + ": " + c);
135 }
136 }
137
138 /**
139 * creates a Square with the specified ghost on it
140 * and appends the placed ghost into the ghost list.
141 *
142 * @param ghosts all the ghosts in the level so far, the new ghost will be appended
143 * @param ghost the newly created ghost to be placed
144 * @return a square with the ghost on it.
145 */
146 protected Square makeGhostSquare(List<Ghost> ghosts, Ghost ghost) {
147 Square ghostSquare = boardCreator.createGround();
148 ghosts.add(ghost);
149 ghost.occupy(ghostSquare);
150 return ghostSquare;
151 }
152
153 /**
154 * Parses the list of strings into a 2-dimensional character array and
155 * passes it on to {@link #parseMap(char[][])}.
156 *
157 * @param text
158 * The plain text, with every entry in the list being a equally
159 * sized row of squares on the board and the first element being
160 * the top row.
161 * @return The level as represented by the text.
162 * @throws PacmanConfigurationException If text lines are not properly formatted.
163 */
164 public Level parseMap(List<String> text) {
165
166 checkMapFormat(text);
167
168 int height = text.size();
169 int width = text.get(0).length();
170
171 char[][] map = new char[width][height];
172 for (int x = 0; x < width; x++) {
173 for (int y = 0; y < height; y++) {
174 map[x][y] = text.get(y).charAt(x);
175 }
176 }
177 return parseMap(map);
178 }
179
180 /**
181 * Check the correctness of the map lines in the text.
182 * @param text Map to be checked
183 * @throws PacmanConfigurationException if map is not OK.
184 */
185 private void checkMapFormat(List<String> text) {
186 if (text == null) {
187 throw new PacmanConfigurationException(
188 "Input text cannot be null.");
189 }
190
191 if (text.isEmpty()) {
192 throw new PacmanConfigurationException(
193 "Input text must consist of at least 1 row.");
194 }
195
196 int width = text.get(0).length();
197
198 if (width == 0) {
199 throw new PacmanConfigurationException(
200 "Input text lines cannot be empty.");
201 }
202
203 for (String line : text) {
204 if (line.length() != width) {
205 throw new PacmanConfigurationException(
206 "Input text lines are not of equal width.");
207 }
208 }
209 }
210
211 /**
212 * Parses the provided input stream as a character stream and passes it
213 * result to {@link #parseMap(List)}.
214 *
215 * @param source
216 * The input stream that will be read.
217 * @return The parsed level as represented by the text on the input stream.
218 * @throws IOException
219 * when the source could not be read.
220 */
221 public Level parseMap(InputStream source) throws IOException {
222 try (BufferedReader reader = new BufferedReader(new InputStreamReader(
223 source, "UTF-8"))) {
224 List<String> lines = new ArrayList<>();
225 while (reader.ready()) {
226 lines.add(reader.readLine());
227 }
228 return parseMap(lines);
229 }
230 }
231
232 /**
233 * Parses the provided input stream as a character stream and passes it
234 * result to {@link #parseMap(List)}.
235 *
236 * @param mapName
237 * Name of a resource that will be read.
238 * @return The parsed level as represented by the text on the input stream.
239 * @throws IOException
240 * when the resource could not be read.
241 */
242 @SuppressFBWarnings(
243 value = {"OBL_UNSATISFIED_OBLIGATION", "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"},
244 justification = "try with resources always cleans up / false positive in java 11"
245 )
246 public Level parseMap(String mapName) throws IOException {
247 try (InputStream boardStream = MapParser.class.getResourceAsStream(mapName)) {
248 if (boardStream == null) {
249 throw new PacmanConfigurationException("Could not get resource for: " + mapName);
250 }
251 return parseMap(boardStream);
252 }
253 }
254
255 /**
256 * @return the BoardCreator
257 */
258 protected BoardFactory getBoardCreator() {
259 return boardCreator;
260 }
261 }