Skip to content

Commit e8885d5

Browse files
committed
Implement scoring
1 parent 8fadb3f commit e8885d5

File tree

4 files changed

+159
-25
lines changed

4 files changed

+159
-25
lines changed

herbert/level.py

+60-16
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,13 @@ def fromfile(cls, file, *, nrows=25, ncols=25):
4747

4848
field = field[:-1]
4949

50-
line = file.readline(5)
51-
if line.endswith('\n'):
52-
line = line[:-1]
50+
points = _readint(file, 7, cls.NUMBER_PATTERN, 1, 1000000, nrows + 1, newline=True)
51+
max_bytes = _readint(file, 4, cls.NUMBER_PATTERN, 1, 1000, nrows + 2)
5352

54-
match = cls.NUMBER_PATTERN.fullmatch(line)
55-
error = ValueError('expected a positive integer in the range [1, 1000) at line %d: %s' % (nrows + 1, line))
53+
return cls(field, points, max_bytes, nrows, ncols)
5654

57-
if match:
58-
max_bytes = int(line)
59-
if max_bytes < 1 or max_bytes >= 1000:
60-
raise error
61-
else:
62-
raise error
63-
64-
return cls(field, max_bytes, nrows, ncols)
65-
66-
def __init__(self, field, max_bytes, nrows, ncols):
55+
def __init__(self, field, points, max_bytes, nrows, ncols):
56+
self.points = points
6757
self.max_bytes = max_bytes
6858
self.nrows = nrows
6959
self.ncols = ncols
@@ -154,6 +144,28 @@ def _parse(self, field):
154144
self.inaccessible_spots = inaccessible_spots
155145

156146

147+
def _readint(file, size, pattern, lo, hi, row, newline=False):
148+
line = file.readline(size)
149+
error = ValueError('expected a positive integer in the range [%d, %d) at line %d: %s' % (lo, hi, row, line))
150+
151+
if line.endswith('\n'):
152+
line = line[:-1]
153+
elif newline:
154+
raise error
155+
156+
match = pattern.fullmatch(line)
157+
158+
if match:
159+
result = int(line)
160+
161+
if result < lo or result >= hi:
162+
raise error
163+
164+
return result
165+
166+
raise error
167+
168+
157169
def _extend_horizontally(grid, hseen, row, col, nrows, ncols):
158170
len = 0
159171
hseen.add((row, col))
@@ -270,6 +282,7 @@ def __init__(self, level, robot, gray_buttons, white_buttons):
270282
self.robot = robot
271283
self.gray_buttons = gray_buttons
272284
self.white_buttons = white_buttons
285+
self.total_buttons = len(white_buttons)
273286
self.npressed = 0 # the number of white buttons pressed
274287
self.max_npressed = 0 # the maximum number of white buttons pressed
275288
self.completed = False # True iff all the white buttons have been pressed
@@ -290,11 +303,42 @@ def step(self, command):
290303
if self.npressed > self.max_npressed:
291304
self.max_npressed = self.npressed
292305

293-
if not self.completed and self.white_buttons and self.npressed == len(self.white_buttons):
306+
if not self.completed and self.white_buttons and self.npressed == self.total_buttons:
294307
self.completed = True
295308
elif command == 'l':
296309
self.robot.turn_left()
297310
elif command == 'r':
298311
self.robot.turn_right()
299312
else:
300313
raise ValueError('not a command: %s' % command)
314+
315+
def score(self, bytes):
316+
return calculate_score(self.level.points, self.level.max_bytes, self.total_buttons, self.npressed, bytes)
317+
318+
319+
def calculate_score(points, max_bytes, total_buttons, buttons, bytes):
320+
"""Calculates the score for a level.
321+
322+
points: the points assigned for solving the level (based on difficulty)
323+
max_bytes: the maximum number of bytes for the level
324+
total_buttons: the number of white buttons on the level
325+
buttons: the number of white buttons pressed
326+
bytes: the number of bytes actually used
327+
"""
328+
329+
if buttons == total_buttons and bytes <= max_bytes:
330+
# level solved
331+
return (points * max_bytes) // bytes
332+
333+
# level unsolved
334+
assert buttons < total_buttons or bytes > max_bytes
335+
336+
points_per_button = 0
337+
338+
if bytes <= max_bytes:
339+
points_per_button = points // (2 * total_buttons)
340+
341+
if max_bytes < bytes <= 2*max_bytes:
342+
points_per_button = points * (2*max_bytes - bytes) // (2 * max_bytes * total_buttons)
343+
344+
return buttons * points_per_button

tests/test_level.py

+33-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def setUp(self):
2020
file.write('..*u*..\n')
2121
file.write('..***..\n')
2222
file.write('.......\n')
23+
file.write('1000\n')
2324

2425
def tearDown(self):
2526
self.file.close()
@@ -32,6 +33,7 @@ def test_when_file_ends_with_a_newline(self):
3233

3334
self.assertEqual(level.nrows, 13)
3435
self.assertEqual(level.ncols, 7)
36+
self.assertEqual(level.points, 1000)
3537
self.assertEqual(level.max_bytes, 10)
3638
self.assertEqual(level.robot, (10, 3, 'u'))
3739

@@ -61,6 +63,7 @@ def test_when_file_does_not_end_with_a_newline(self):
6163

6264
level = Level.fromfile(self.file, nrows=13, ncols=7)
6365

66+
self.assertEqual(level.points, 1000)
6467
self.assertEqual(level.max_bytes, 10)
6568

6669

@@ -114,43 +117,64 @@ def test_when_line_too_long(self):
114117
with self.assertRaisesRegex(ValueError, 'line 2 is not 1 character long'):
115118
Level.fromfile(self.file, nrows=3, ncols=1)
116119

117-
def test_when_max_bytes_non_positive(self):
118-
self.file.write('.\n0')
120+
def test_when_points_non_positive(self):
121+
self.file.write('.\n0\n1')
119122
self.file.seek(0)
120123

121124
with self.assertRaisesRegex(ValueError, 'expected a positive integer .* at line 2: 0'):
122125
Level.fromfile(self.file, nrows=1, ncols=1)
123126

127+
def test_when_points_too_large(self):
128+
self.file.write('.\n1000000\n1')
129+
self.file.seek(0)
130+
131+
with self.assertRaisesRegex(ValueError, 'expected a positive integer in the range \[1, 1000000\) at line 2: 1000000'):
132+
Level.fromfile(self.file, nrows=1, ncols=1)
133+
134+
def test_when_points_not_a_number(self):
135+
self.file.write('.\na\n1')
136+
self.file.seek(0)
137+
138+
with self.assertRaisesRegex(ValueError, 'expected a positive integer .* at line 2: a'):
139+
Level.fromfile(self.file, nrows=1, ncols=1)
140+
141+
def test_when_max_bytes_non_positive(self):
142+
self.file.write('.\n1\n0')
143+
self.file.seek(0)
144+
145+
with self.assertRaisesRegex(ValueError, 'expected a positive integer .* at line 3: 0'):
146+
Level.fromfile(self.file, nrows=1, ncols=1)
147+
124148
def test_when_max_bytes_too_large(self):
125-
self.file.write('.\n1000')
149+
self.file.write('.\n1\n1000')
126150
self.file.seek(0)
127151

128-
with self.assertRaisesRegex(ValueError, 'expected a positive integer in the range \[1, 1000\) at line 2: 1000'):
152+
with self.assertRaisesRegex(ValueError, 'expected a positive integer in the range \[1, 1000\) at line 3: 1000'):
129153
Level.fromfile(self.file, nrows=1, ncols=1)
130154

131155
def test_when_max_bytes_not_a_number(self):
132-
self.file.write('.\na')
156+
self.file.write('.\n1\na')
133157
self.file.seek(0)
134158

135-
with self.assertRaisesRegex(ValueError, 'expected a positive integer .* at line 2: a'):
159+
with self.assertRaisesRegex(ValueError, 'expected a positive integer .* at line 3: a'):
136160
Level.fromfile(self.file, nrows=1, ncols=1)
137161

138162
def test_when_too_many_robots(self):
139-
self.file.write('.u.\n..d\n5')
163+
self.file.write('.u.\n..d\n1\n1')
140164
self.file.seek(0)
141165

142166
with self.assertRaisesRegex(ValueError, 'too many robots, .* \(2, 3\)'):
143167
Level.fromfile(self.file, nrows=2, ncols=3)
144168

145169
def test_when_improper_wall(self):
146-
self.file.write('..**.*.\n1')
170+
self.file.write('..**.*.\n1\n1')
147171
self.file.seek(0)
148172

149173
with self.assertRaisesRegex(ValueError, 'improper wall at \(1, 6\)'):
150174
Level.fromfile(self.file, nrows=1, ncols=7)
151175

152176
def test_when_no_robot(self):
153-
self.file.write('..****.\n1')
177+
self.file.write('..****.\n1\n1')
154178
self.file.seek(0)
155179

156180
with self.assertRaisesRegex(ValueError, 'no robot found'):

tests/test_runtime_environment.py

+32
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def setUp(self):
1212
file.write('.*r.w.g.w.\n')
1313
file.write('.***......\n')
1414
file.write('..........\n')
15+
file.write('50\n')
1516
file.write('11')
1617
file.seek(0)
1718

@@ -131,3 +132,34 @@ def test_sslsrssssrsrss(self):
131132
self.assertFalse(self.re.white_buttons[(2, 8)].pressed)
132133
self.assertEqual(self.re.max_npressed, 2)
133134
self.assertTrue(self.re.completed)
135+
136+
137+
class ScoreTestCase(unittest.TestCase):
138+
def test_example1(self):
139+
file = io.StringIO()
140+
file.write('.r.wwwwwwwwwwwwwwwwwwww.\n')
141+
file.write('100\n')
142+
file.write('10\n')
143+
file.seek(0)
144+
145+
level = Level.fromfile(file, nrows=1, ncols=24)
146+
re = level()
147+
148+
for _ in range(25):
149+
re.step('s')
150+
151+
# points = 100
152+
# max_bytes = 10
153+
# total_buttons = 20
154+
# buttons = 20
155+
156+
# bytes = 10
157+
self.assertEqual(re.score(10), 100)
158+
159+
# bytes = 5
160+
self.assertEqual(re.score(5), 200)
161+
162+
# bytes = 15
163+
self.assertEqual(re.score(15), 20)
164+
165+
file.close()

tests/test_scoring.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
3+
from herbert.level import calculate_score
4+
5+
6+
class ScoringTestCase(unittest.TestCase):
7+
def setUp(self):
8+
self.points = 1000
9+
self.max_bytes = 20
10+
self.total_buttons = 50
11+
12+
def test_example1(self):
13+
buttons = 50
14+
bytes = 20
15+
16+
self.assertEqual(calculate_score(self.points, self.max_bytes, self.total_buttons, buttons, bytes), 1000)
17+
18+
def test_example2(self):
19+
buttons = 25
20+
bytes = 20
21+
22+
self.assertEqual(calculate_score(self.points, self.max_bytes, self.total_buttons, buttons, bytes), 250)
23+
24+
def test_example3(self):
25+
buttons = 50
26+
bytes = 30
27+
28+
self.assertEqual(calculate_score(self.points, self.max_bytes, self.total_buttons, buttons, bytes), 250)
29+
30+
def test_example4(self):
31+
buttons = 50
32+
bytes = 10
33+
34+
self.assertEqual(calculate_score(self.points, self.max_bytes, self.total_buttons, buttons, bytes), 2000)

0 commit comments

Comments
 (0)