diff --git a/robotgame/logic/direction.py b/robotgame/logic/direction.py index e944bf3..77ddb43 100644 --- a/robotgame/logic/direction.py +++ b/robotgame/logic/direction.py @@ -52,3 +52,14 @@ class Left(Direction): def next_pos(pos): x, y = pos return x - 1, y + +succ = {Up: Right, + Right: Down, + Down: Left, + Left: Up}.__getitem__ + +pred = {Right: Up, + Down: Right, + Left: Down, + Up: Left}.__getitem__ + diff --git a/robotgame/logic/rollingstone.py b/robotgame/logic/rollingstone.py index 8975678..09ba47f 100644 --- a/robotgame/logic/rollingstone.py +++ b/robotgame/logic/rollingstone.py @@ -22,13 +22,21 @@ # copyright : (C) 2012 Niels G. W. Serup # maintained by : Niels G. W. Serup -"""Logic for rolling.""" +""" +Logic for a rolling stone on a playfield of movement-stopping stones and +direction-changing turns. Also has a pseudo-random playfield generator. +""" from __future__ import print_function +from robotgame.logic.direction import * +import random class RollingStoneError(Exception): pass +class WouldHitWall(RollingStoneError): + pass + class Field(object): def next_posdir(self): raise NotImplementedError @@ -56,21 +64,34 @@ class Stone(Field): return pos, direc -def step(playfield, pos, direc): - field = _at(playfield, pos) +def step(playfield, old_pos, old_direc): + """ + Return a new (position, direction) tuple based on the location on the + playfield. + """ + field = _at(playfield, old_pos) if field is not None: - return field.next_posdir(pos, direc) - return direc.next_pos(pos), direc + (x, y), direc = field.next_posdir(old_pos, old_direc) + else: + (x, y), direc = old_direc.next_pos(old_pos), old_direc + + if x < 0 or x >= len(playfield[y]) or y < 0 or y >= len(playfield): + return old_pos, old_direc + return (x, y), direc def reaches_goal(playfield, max_steps): + """ + Determine if the rolling stone reaches the goal within range(max_steps). + """ pos = _find_start(playfield) direc = None for i in range(max_steps): - pos, direc = step(playfield, pos, direc) + new_pos, new_direc = step(playfield, pos, direc) if isGoal(playfield, pos): return True - if isStone(playfield, pos): + if new_pos == pos: return False + pos, direc = new_pos, new_direc return False def _find_start(playfield): @@ -84,10 +105,80 @@ def _at(playfield, pos): x, y = pos return playfield[y][x] +def _set(playfield, pos, val): + x, y = pos + playfield[y][x] = val + _is = lambda t: lambda playfield, pos: isinstance(_at(playfield, pos), t) -(isGoal, isTurn, isStart, isStone) = (_is(Goal), _is(Turn), _is(Start), _is(Stone)) +isGoal, isTurn, isStart, isStone = _is(Goal), _is(Turn), _is(Start), _is(Stone) +def generate_playfield(height, width, start_pos, start_direc, goal_pos, nstones, nturns=None): + """ + Generate a completable playfield. + + The generated playfield will have nstones stones nturns turns. A + completable playfield will always be completable in either zero, one, or + two steps. + """ + playfield = [[None for i in range(width)] for i in range(height)] + _set(playfield, start_pos, Start(start_direc)) + _set(playfield, goal_pos, Goal()) + + def _find_min_turns(from_pos, from_direc): + x0, y0 = from_pos + x2, y2 = goal_pos + turns = [] + if from_direc in (Up, Left): + def get_turns(x0, y0, x2, y2): + if y0 == 0: + raise WouldHitWall + elif y0 < y2: + turns.append(((x0, y0 - 1), succ(succ(from_direc)))) + turns.extend(_find_min_turns(*turns[-1])) + elif y0 > y2 and x0 != x2: + turns.append(((x0, y2), succ(from_direc) if x0 < x2 else pred(from_direc))) + elif y0 == y2 and x0 != x2: + turns.append(((x0, y0 - 1), succ(from_direc) if x0 < x2 else pred(from_direc))) + turns.append(((x2, y0 - 1), succ(succ(from_direc)))) + return turns + if from_direc is Up: + turns = get_turns(x0, y0, x2, y2) + else: + turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)] + else: + def get_turns(x0, y0, x2, y2): + if x0 > x2: + turns.append(((x0 + 1, y0), succ(succ(from_direc)))) + turns.extend(_find_min_turns(*turns[-1])) + elif x0 < x2 and y0 != y2: + turns.append(((x2, y0), pred(from_direc) if y0 < y2 else succ(from_direc))) + elif x0 == x2 and y0 != y2: + turns.append(((x0 + 1, y0), pred(from_direc) if y0 < y2 else succ(from_direc))) + turns.append(((x0 + 1, y2), succ(succ(from_direc)))) + return turns + if from_direc is Right: + if x0 == len(playfield[y0]): + raise WouldHitWall + turns = get_turns(x0, y0, x2, y2) + else: + if y0 == len(playfield): + raise WouldHitWall + turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)] + return turns + + def _randomize_path(turns): + pass + + def _insert_stones(turns): + pass + + turns = _find_min_turns(start_pos, start_direc) + if nturns is not None: + if len(turns) > nturns: + raise RollingStoneError("Too few steps allocated.") + _randomize_path(turns) + _insert_stones() + return playfield, 3 -def generate_playfield(): diff --git a/tests/rollingstone_tests.py b/tests/rollingstone_tests.py index e728447..267899a 100644 --- a/tests/rollingstone_tests.py +++ b/tests/rollingstone_tests.py @@ -23,5 +23,9 @@ class RollingStoneTest(unittest.TestCase): self.assertTrue(reaches_goal(playfield_example_succeed, 100)) self.assertFalse(reaches_goal(playfield_example_fail, 100)) + def test_playfield_generation(self): + playfield, min_steps = generate_playfield(10, 10, (0, 0), Down, (9, 9), 10, 5) + # self.assertTrue(reaches_goal(playfield, min_steps)) + if __name__ == '__main__': unittest.main()