From 6c050f954b8afb4a670493946ef71f8bff57aafa Mon Sep 17 00:00:00 2001 From: Niels Serup Date: Wed, 8 Aug 2012 11:07:33 +0200 Subject: [PATCH] Finished rolling stone playfield generation. --- .gitignore | 3 +- robotgame/logic/direction.py | 12 +- robotgame/logic/rollingstone.py | 278 ++++++++++++++++++-------------- tests/rollingstone_tests.py | 44 ++--- 4 files changed, 184 insertions(+), 153 deletions(-) diff --git a/.gitignore b/.gitignore index 37a1f59..182889a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ *~ -*.pyc -*.pyo \ No newline at end of file +*.py[co] diff --git a/robotgame/logic/direction.py b/robotgame/logic/direction.py index 77ddb43..d747b2c 100644 --- a/robotgame/logic/direction.py +++ b/robotgame/logic/direction.py @@ -53,13 +53,9 @@ class Left(Direction): x, y = pos return x - 1, y -succ = {Up: Right, - Right: Down, - Down: Left, - Left: Up}.__getitem__ +all_directions = set((Up, Left, Down, Right)) -pred = {Right: Up, - Down: Right, - Left: Down, - Up: Left}.__getitem__ +succ = lambda d: all_directions[(all_directions.index(d) + 1) % 4] +pred = lambda d: all_directions[(all_directions.index(d) - 1) % 4] +isDirection = lambda obj: obj in (Up, Left, Down, Right) diff --git a/robotgame/logic/rollingstone.py b/robotgame/logic/rollingstone.py index 09ba47f..336b342 100644 --- a/robotgame/logic/rollingstone.py +++ b/robotgame/logic/rollingstone.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- # This file is part of ROBOTGAME # @@ -28,157 +29,184 @@ direction-changing turns. Also has a pseudo-random playfield generator. """ from __future__ import print_function +import math +import itertools from robotgame.logic.direction import * import random class RollingStoneError(Exception): pass -class WouldHitWall(RollingStoneError): +class Stone(object): pass -class Field(object): - def next_posdir(self): - raise NotImplementedError - -class Start(Field): - def __init__(self, direction): - self.direction = direction - - def next_posdir(self, pos, direc): - return self.direction.next_pos(pos), self.direction - -class Turn(Field): - def __init__(self, direction): - self.direction = direction - - def next_posdir(self, pos, direc): - return self.direction.next_pos(pos), self.direction - -class Goal(Field): - def next_posdir(self, pos, direc): - return pos, direc - -class Stone(Field): - def next_posdir(self, pos, direc): - return pos, direc - - -def step(playfield, old_pos, old_direc): +def step(playfield, width, height, old_pos, direc): """ Return a new (position, direction) tuple based on the location on the playfield. """ - field = _at(playfield, old_pos) - if field is not None: - (x, y), direc = field.next_posdir(old_pos, old_direc) - else: - (x, y), direc = old_direc.next_pos(old_pos), old_direc + pos = direc.next_pos(old_pos) + x, y = pos + if playfield.get(pos) is Stone or x < 0 or x >= width \ + or y < 0 or y >= height: + pos = old_pos + elif isDirection(playfield.get(pos)): + direc = playfield[pos] + return pos, 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, width, height, max_steps, start_pos, goal_pos): + """ + Determine if the rolling stone reaches the goal within max_steps steps. -def reaches_goal(playfield, max_steps): + playfield[start_pos] must contain either a Turn(Down) or a Turn(Right) + object, or the rolling stone will not roll. """ - Determine if the rolling stone reaches the goal within range(max_steps). - """ - pos = _find_start(playfield) - direc = None - for i in range(max_steps): - new_pos, new_direc = step(playfield, pos, direc) - if isGoal(playfield, pos): + pos = start_pos + direc = playfield[pos] + for _ in range(max_steps): + new_pos, new_direc = step(playfield, width, height, pos, direc) + if new_pos == goal_pos: return True if new_pos == pos: return False pos, direc = new_pos, new_direc return False -def _find_start(playfield): - for y in range(len(playfield)): - for x in range(len(playfield[y])): - if isStart(playfield, (x, y)): - return (x, y) - raise RollingStoneError("Missing Start field") -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) - - -def generate_playfield(height, width, start_pos, start_direc, goal_pos, nstones, nturns=None): +def generate_simple_playfield(width, height, nturns, nstones): """ - Generate a completable playfield. + Generate a completable playfield where: + * the starting position is in the upper left corner + * the goal is in the lower right corner + * the playfield is completable in nturns or less + * the playfield has at most nstones stones - The generated playfield will have nstones stones nturns turns. A - completable playfield will always be completable in either zero, one, or - two steps. + Return (playfield : {(x, y): Direction | Stone}, + steps : int) + where (x, y) : (int, int) + + The returned playfield contains Direction objects which can be used with + the step function to move towards the goal. The solution denoted by the + Direction objects is not necessarily the only solution. + + 'steps' is the number of steps used by the generated solution. It is not + necessarily the lowest number of steps the playfield can be completed in. """ - 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)] + min_width, min_height = _min_play_size(nturns) + if width < min_width or height < min_height: + nturns = min(2 * (width - 1), 2 * (height - 1) - 1) + min_width, min_height = _min_play_size(nturns) + + do_transpose = random.choice((True, False)) + if do_transpose: + width, height = height, width + + turns, stones = [((0, 0), None)], [] + x, y = (0, 0) + not_allowed_y = [] + offset_x = 0 + while True: + missing = nturns - len(turns) + 1 + if missing == 1: + turns[-1] = (turns[-1][0], Down) + turns.append(((x, height - 1), Right)) + break + elif missing == 0: + break 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 + allowed = set(range(0, height)) - set(not_allowed_y) + if missing == 3: + allowed -= set((height - 1,)) + if missing == nturns: + allowed -= set((0,)) + y1 = random.choice(list(allowed)) + turns[-1] = (turns[-1][0], Down if y1 > y else Up) + not_allowed_y.append(y1) + if len(not_allowed_y) == 3: + del not_allowed_y[0] + turns.append(((x, y1), Right)) + x1p = random.randint(0, width - min_width - offset_x) + offset_x += x1p + x1 = x + x1p + 1 + turns.append(((x1, y1), None)) + x, y = x1, y1 + turns.append(((width - 1, height - 1), None)) - 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 + if do_transpose: + turns[:] = [((y, x), { + Down: Right, + Right: Down, + Up: Left, + }.get(d)) for ((x, y), d) in turns] + used_fields = _fields_from_turns(turns) + playfield = {} + for p, d in turns: + playfield[p] = d + emptys = set(itertools.product(range(width), + range(height))) - set(used_fields) + for _ in range(nstones): + if not emptys: + break + pos = random.choice(list(emptys)) + emptys.remove(pos) + playfield[pos] = Stone + return playfield, len(used_fields) - 1 + +def generate_simple_unsolved_solvable_playfield(width, height, nturns, nstones): + """ + Return a tuple of a playfield without direction objects, and a list of the + direction objects. + """ + playfield = generate_simple_playfield(width, height, nturns, stones) + new_playfield, directions = {}, [] + for pos, val in playfield.items(): + if val is Stone: + new_playfield[pos] = val + else: + directions.append(val) + return new_playfield, directions + +def generate_simple_unsolved_solvable_extra(width, height, nturns, nstones): + """ + Do the same as generate_simple_unsolved_solvable, but throw in some copies + of the direction object not returned by that function. You probably want to + use this in your game. + """ + playfield, directions = generate_simple_unsolved_solvable( + width, height, nturns, nstones) + missing_dir = list(all_directions - set(directions))[0] + return playfield, directions + [missing_dir] * (len(directions) / 3) + +def print_playfield(playfield, width, height, hide_directions): + text = [['ยท' for _ in range(width)] for _ in range(height)] + for (x, y), val in playfield.items(): + if isDirection(val) and hide_directions: + continue + text[y][x] = '%' if val == Stone else repr(val).rsplit('.', 1)[1][0] \ + if isDirection(val) else 'G' + print('\n'.join(''.join(line) for line in text)) + +def _cells_upto(fields, start, direc, end): + (x0, y0), (x2, y2) = start, end + if direc in (Up, Down): + t = -1 if direc == Up else 1 + for y in range(y0 + t, y2 + t, t): + fields.append((x0, y)) + else: + t = -1 if direc == Left else 1 + for x in range(x0 + t, x2 + t, t): + fields.append((x, y0)) + +def _fields_from_turns(turns): + fields = [(0, 0)] + prev_pos, prev_direc = turns[0] + for (pos, direc) in turns: + _cells_upto(fields, prev_pos, prev_direc, pos) + prev_pos, prev_direc = pos, direc + return fields + +def _min_play_size(nturns): + return (int(math.ceil(nturns / 2.0)) + 1, + int(math.ceil((nturns + 1) / 2.0)) + 1) diff --git a/tests/rollingstone_tests.py b/tests/rollingstone_tests.py index 267899a..172d84b 100644 --- a/tests/rollingstone_tests.py +++ b/tests/rollingstone_tests.py @@ -1,31 +1,39 @@ +from __future__ import print_function import unittest from robotgame.logic.rollingstone import * from robotgame.logic.direction import * -playfield_example_succeed = [ - [Start(Down), None, None, None ], - [None, None, Stone(), None ], - [Turn(Right), None, Turn(Down), None ], - [None, Stone(), Turn(Right), Goal()], - ] - -playfield_example_fail = [ - [Start(Down), None, None, None ], - [None, None, Stone(), None ], - [Turn(Right), Stone(), Turn(Down), None ], - [None, None, Turn(Right), Goal()], - ] - class RollingStoneTest(unittest.TestCase): def test_playfield(self): - self.assertTrue(reaches_goal(playfield_example_succeed, 100)) - self.assertFalse(reaches_goal(playfield_example_fail, 100)) + playfield_example_succeed = { + (0, 0): Down, + (0, 2): Right, + (1, 3): Stone, + (2, 1): Stone, + (2, 2): Down, + (2, 3): Right, + } + self.assertTrue(reaches_goal(playfield_example_succeed, + 4, 4, 100, (0, 0), (3, 3))) + + playfield_example_succeed[(1, 2)] = Stone + self.assertFalse(reaches_goal(playfield_example_succeed, + 4, 4, 100, (0, 0), (3, 3))) 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)) + print() + playfield, steps = generate_simple_playfield(10, 10, 100, 100) + print_playfield(playfield, 10, 10, True) + self.assertTrue( + reaches_goal(playfield, 10, 10, steps, (0, 0), (9, 9))) + + print() + playfield, steps = generate_simple_playfield(10, 10, 7, 20) + print_playfield(playfield, 10, 10, True) + self.assertTrue( + reaches_goal(playfield, 10, 10, steps, (0, 0), (9, 9))) if __name__ == '__main__': unittest.main()