-
Notifications
You must be signed in to change notification settings - Fork 1
/
KarelWorld.py
336 lines (269 loc) · 9.63 KB
/
KarelWorld.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
"""
This file defines the class definition of a Karel world.
The sub header comment defines important notes about the Karel
world file format.
Original Author: Nicholas Bowman
Credits: Kylie Jue
License: MIT
Version: 1.0.0
Email: nbowman@stanford.edu
Date of Creation: 10/1/2019
Last Modified: 3/31/2020
"""
"""
General Notes About World Construction
- Streets run EAST-WEST (rows)
- Avenues run NORTH-SOUTH (columns)
World File Constraints:
- World file should specify one component per line in the format
KEYWORD: PARAMETERS
- Any lines with no colon delimiter will be ignored
- The accepted KEYWORD, PARAMETER combinations are as follows:
- Dimension: (num_avenues, num_streets)
- Wall: (avenue, street); direction
- Beeper: (avenue, street) count
- Karel: (avenue, street); direction
- Color: (avenue, street); color
- Speed: delay
- BeeperBag: num_beepers
- Multiple parameter values for the same keyword should be separated by a semicolon
- All numerical values (except delay) must be expressed as ints. The exception
to this is that the number of beepers can also be INFINITY
- Any specified color values must be valid TKinter color strings, and are limited
to the set of colors
- Direction is case-insensitive and can be one of the following values:
- East
- West
- North
- South
"""
from karel.kareldefinitions import *
import collections
import re
import copy
class KarelWorld():
def __init__(self, world_file=None):
"""
Karel World constructor
Parameters:
world_file: Open file object containing information about the initial state of Karel's world
"""
self._world_file = world_file
# Map of beeper locations to the count of beepers at that location
self._beepers = collections.defaultdict(int)
# Map of corner colors, defaults to None
self._corner_colors = collections.defaultdict(lambda: "")
# Set of Wall objects placed in the world
self._walls = set()
# Dimensions of the world
self._num_streets = 1
self._num_avenues = 1
# Initial Karel state saved to enable world reset
self._karel_starting_location = (1, 1)
self._karel_starting_direction = Direction.EAST
self._karel_starting_beeper_count = 0
# Initial speed slider setting
self._init_speed = INIT_SPEED
# If a world file has been specified, load world details from the file
if self._world_file:
self.load_from_file()
# Save initial beeper state to enable world reset
self._init_beepers = copy.deepcopy(self._beepers)
@property
def karel_starting_location(self):
return self._karel_starting_location
@property
def karel_starting_direction(self):
return self._karel_starting_direction
@property
def karel_starting_beeper_count(self):
return self._karel_starting_beeper_count
@property
def init_speed(self):
return self._init_speed
@property
def num_streets(self):
return self._num_streets
@num_streets.setter
def num_streets(self, val):
self._num_streets = val
@property
def num_avenues(self):
return self._num_avenues
@num_avenues.setter
def num_avenues(self, val):
self._num_avenues = val
@property
def beepers(self):
return self._beepers
@property
def corner_colors(self):
return self._corner_colors
@property
def walls(self):
return self._walls
def load_from_file(self):
def parse_line(line):
# Ignore blank lines and lines with no comma delineator
if not line or ":" not in line:
return None, None, False
params = {}
components = line.strip().split(KEYWORD_DELIM)
keyword = components[0].lower()
# only accept valid keywords as defined in world file spec
if keyword not in VALID_WORLD_KEYWORDS:
return None, None, False
param_list = components[1].split(PARAM_DELIM)
for param in param_list:
param = param.strip().lower()
# first check to see if the parameter is a direction value
if param in VALID_DIRECTIONS:
params["dir"] = DIRECTIONS_MAP[param]
else:
# next check to see if parameter encodes a location
coordinate = re.match(r"\((\d+),\s*(\d+)\)", param)
if coordinate:
avenue = int(coordinate.group(1))
street = int(coordinate.group(2))
params["loc"] = (avenue, street)
else:
# finally check to see if parameter encodes a numerical value or color string
val = None
if param.isdigit():
val = int(param)
elif keyword == "speed":
# double values are only allowed for speed parameter
try:
val = int(100 * float(param))
except ValueError:
# invalid parameter value, do not process
continue
elif keyword == "beeperbag":
# handle the edge case where Karel has infinite beepers
if param == "infinity" or param == "infinite":
val = INFINITY
elif keyword == "color":
# TODO: add check for valid color?
val = param
# only store non-null values
if val is not None: params["val"] = val
return keyword.lower(), params, True
for i, line in enumerate(self._world_file):
keyword, params, is_valid = parse_line(line)
# skip invalid lines (comments, incorrectly formatted, invalid keyword)
if not is_valid:
# print(f"Ignoring line {i} of world file: {line.strip()}")
continue
# TODO: add error detection for keywords with insufficient parameters
# handle all different possible keyword cases
if keyword == "dimension":
# set world dimensions based on location values
self._num_avenues, self._num_streets = params["loc"]
elif keyword == "wall":
# build a wall at the specified location
avenue, street = params["loc"]
direction = params["dir"]
self._walls.add(Wall(avenue, street, direction))
elif keyword == "beeper":
# add the specified number of beepers to the world
avenue, street = params["loc"]
count = params["val"]
self._beepers[(avenue, street)] += count
elif keyword == "karel":
# Give Karel initial state values
avenue, street = params["loc"]
direction = params["dir"]
self._karel_starting_location = (avenue, street)
self._karel_starting_direction = direction
elif keyword == "beeperbag":
# Set Karel's initial beeper bag count
count = params["val"]
self._karel_starting_beeper_count = count
elif keyword == "speed":
# Set delay speed of program execution
speed = params["val"]
self._init_speed = speed
elif keyword == "color":
# Set corner color to be specified color
avenue, street = params["loc"]
color = params["val"]
self._corner_colors[(avenue, street)] = color
def add_beeper(self, avenue, street):
self._beepers[(avenue, street)] += 1
def remove_beeper(self, avenue, street):
if self._beepers[(avenue, street)] == 0:
return
self._beepers[(avenue, street)] -= 1
def add_wall(self, wall):
alt_wall = self.get_alt_wall(wall)
if wall not in self._walls and alt_wall not in self._walls:
self._walls.add(wall)
def remove_wall(self, wall):
alt_wall = self.get_alt_wall(wall)
if wall in self._walls:
self._walls.remove(wall)
if alt_wall in self._walls:
self._walls.remove(alt_wall)
def get_alt_wall(self, wall):
if wall.direction == Direction.NORTH:
return Wall(wall.avenue, wall.street + 1, Direction.SOUTH)
if wall.direction == Direction.SOUTH:
return Wall(wall.avenue, wall.street - 1, Direction.NORTH)
if wall.direction == Direction.EAST:
return Wall(wall.avenue + 1, wall.street, Direction.WEST)
if wall.direction == Direction.WEST:
return Wall(wall.avenue - 1, wall.street, Direction.EAST)
def paint_corner(self, avenue, street, color):
self._corner_colors[(avenue, street)] = color
def corner_color(self, avenue, street):
return self._corner_colors[(avenue, street)]
def reset_corner(self, avenue, street):
self._beepers[(avenue, street)] = 0
self._corner_colors[(avenue, street)] = ""
def wall_exists(self, avenue, street, direction):
wall = Wall(avenue, street, direction)
return wall in self._walls
def in_bounds(self, avenue, street):
return avenue > 0 and street > 0 and avenue <= self._num_avenues and street <= self._num_streets
def reset_world(self):
"""
Reset initial state of beepers in the world
"""
self._beepers = copy.deepcopy(self._init_beepers)
self._corner_colors = collections.defaultdict(lambda: "")
def reload_world(self, filename=None):
"""
TODO: Do better decomp to not just copy constructor
"""
self._beepers = collections.defaultdict(int)
self._corner_colors = collections.defaultdict(lambda: "")
self._walls = set()
self._num_streets = 1
self._num_avenues = 1
self._karel_starting_location = (1, 1)
self._karel_starting_direction = Direction.EAST
self._karel_starting_beeper_count = 0
self._init_speed = INIT_SPEED
if filename:
self._world_file = open(filename, 'r')
self.load_from_file()
self._init_beepers = copy.deepcopy(self._beepers)
def save_to_file(self, filename, karel):
with open(filename, "w") as f:
# First, output dimensions of world
f.write(f"Dimension: ({self.num_avenues}, {self.num_streets})\n")
# Next, output all walls
for wall in self._walls:
f.write(f"Wall: ({wall.avenue}, {wall.street}); {DIRECTIONS_MAP_INVERSE[wall.direction]}\n")
# Next, output all beepers
for loc, count in self._beepers.items():
f.write(f"Beeper: ({loc[0]}, {loc[1]}); {count}\n")
# Next, output all color information
for loc, color in self._corner_colors.items():
if color:
f.write(f"Color: ({loc[0]}, {loc[1]}); {color}\n")
# Next, output Karel information
f.write(f"Karel: ({karel.avenue}, {karel.street}); {DIRECTIONS_MAP_INVERSE[karel.direction]}\n")
# Finally, output beeperbag info
beeper_output = karel.num_beepers if karel.num_beepers >= 0 else "INFINITY"
f.write(f"BeeperBag: {beeper_output}\n")