Finished rolling stone playfield generation.

This commit is contained in:
Niels Serup 2012-08-08 11:07:33 +02:00
parent 2457f743cc
commit 6c050f954b
4 changed files with 184 additions and 153 deletions

3
.gitignore vendored
View File

@ -1,3 +1,2 @@
*~ *~
*.pyc *.py[co]
*.pyo

View File

@ -53,13 +53,9 @@ class Left(Direction):
x, y = pos x, y = pos
return x - 1, y return x - 1, y
succ = {Up: Right, all_directions = set((Up, Left, Down, Right))
Right: Down,
Down: Left,
Left: Up}.__getitem__
pred = {Right: Up, succ = lambda d: all_directions[(all_directions.index(d) + 1) % 4]
Down: Right, pred = lambda d: all_directions[(all_directions.index(d) - 1) % 4]
Left: Down,
Up: Left}.__getitem__
isDirection = lambda obj: obj in (Up, Left, Down, Right)

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of ROBOTGAME # 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 from __future__ import print_function
import math
import itertools
from robotgame.logic.direction import * from robotgame.logic.direction import *
import random import random
class RollingStoneError(Exception): class RollingStoneError(Exception):
pass pass
class WouldHitWall(RollingStoneError): class Stone(object):
pass pass
class Field(object): def step(playfield, width, height, old_pos, direc):
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):
""" """
Return a new (position, direction) tuple based on the location on the Return a new (position, direction) tuple based on the location on the
playfield. playfield.
""" """
field = _at(playfield, old_pos) pos = direc.next_pos(old_pos)
if field is not None: x, y = pos
(x, y), direc = field.next_posdir(old_pos, old_direc) if playfield.get(pos) is Stone or x < 0 or x >= width \
else: or y < 0 or y >= height:
(x, y), direc = old_direc.next_pos(old_pos), old_direc 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): def reaches_goal(playfield, width, height, max_steps, start_pos, goal_pos):
return old_pos, old_direc """
return (x, y), direc 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 = start_pos
""" direc = playfield[pos]
pos = _find_start(playfield) for _ in range(max_steps):
direc = None new_pos, new_direc = step(playfield, width, height, pos, direc)
for i in range(max_steps): if new_pos == goal_pos:
new_pos, new_direc = step(playfield, pos, direc)
if isGoal(playfield, pos):
return True return True
if new_pos == pos: if new_pos == pos:
return False return False
pos, direc = new_pos, new_direc pos, direc = new_pos, new_direc
return False 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): def generate_simple_playfield(width, height, nturns, nstones):
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):
""" """
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 Return (playfield : {(x, y): Direction | Stone},
completable playfield will always be completable in either zero, one, or steps : int)
two steps. 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): min_width, min_height = _min_play_size(nturns)
x0, y0 = from_pos if width < min_width or height < min_height:
x2, y2 = goal_pos nturns = min(2 * (width - 1), 2 * (height - 1) - 1)
turns = [] min_width, min_height = _min_play_size(nturns)
if from_direc in (Up, Left):
def get_turns(x0, y0, x2, y2): do_transpose = random.choice((True, False))
if y0 == 0: if do_transpose:
raise WouldHitWall width, height = height, width
elif y0 < y2:
turns.append(((x0, y0 - 1), succ(succ(from_direc)))) turns, stones = [((0, 0), None)], []
turns.extend(_find_min_turns(*turns[-1])) x, y = (0, 0)
elif y0 > y2 and x0 != x2: not_allowed_y = []
turns.append(((x0, y2), succ(from_direc) if x0 < x2 else pred(from_direc))) offset_x = 0
elif y0 == y2 and x0 != x2: while True:
turns.append(((x0, y0 - 1), succ(from_direc) if x0 < x2 else pred(from_direc))) missing = nturns - len(turns) + 1
turns.append(((x2, y0 - 1), succ(succ(from_direc)))) if missing == 1:
return turns turns[-1] = (turns[-1][0], Down)
if from_direc is Up: turns.append(((x, height - 1), Right))
turns = get_turns(x0, y0, x2, y2) break
else: elif missing == 0:
turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)] break
else: else:
def get_turns(x0, y0, x2, y2): allowed = set(range(0, height)) - set(not_allowed_y)
if x0 > x2: if missing == 3:
turns.append(((x0 + 1, y0), succ(succ(from_direc)))) allowed -= set((height - 1,))
turns.extend(_find_min_turns(*turns[-1])) if missing == nturns:
elif x0 < x2 and y0 != y2: allowed -= set((0,))
turns.append(((x2, y0), pred(from_direc) if y0 < y2 else succ(from_direc))) y1 = random.choice(list(allowed))
elif x0 == x2 and y0 != y2: turns[-1] = (turns[-1][0], Down if y1 > y else Up)
turns.append(((x0 + 1, y0), pred(from_direc) if y0 < y2 else succ(from_direc))) not_allowed_y.append(y1)
turns.append(((x0 + 1, y2), succ(succ(from_direc)))) if len(not_allowed_y) == 3:
return turns del not_allowed_y[0]
if from_direc is Right: turns.append(((x, y1), Right))
if x0 == len(playfield[y0]): x1p = random.randint(0, width - min_width - offset_x)
raise WouldHitWall offset_x += x1p
turns = get_turns(x0, y0, x2, y2) x1 = x + x1p + 1
else: turns.append(((x1, y1), None))
if y0 == len(playfield): x, y = x1, y1
raise WouldHitWall turns.append(((width - 1, height - 1), None))
turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)]
return turns
def _randomize_path(turns): if do_transpose:
pass turns[:] = [((y, x), {
Down: Right,
Right: Down,
Up: Left,
}.get(d)) for ((x, y), d) in turns]
def _insert_stones(turns): used_fields = _fields_from_turns(turns)
pass 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
turns = _find_min_turns(start_pos, start_direc) def generate_simple_unsolved_solvable_playfield(width, height, nturns, nstones):
if nturns is not None: """
if len(turns) > nturns: Return a tuple of a playfield without direction objects, and a list of the
raise RollingStoneError("Too few steps allocated.") direction objects.
_randomize_path(turns) """
_insert_stones() playfield = generate_simple_playfield(width, height, nturns, stones)
return playfield, 3 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)

View File

@ -1,31 +1,39 @@
from __future__ import print_function
import unittest import unittest
from robotgame.logic.rollingstone import * from robotgame.logic.rollingstone import *
from robotgame.logic.direction 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): class RollingStoneTest(unittest.TestCase):
def test_playfield(self): def test_playfield(self):
self.assertTrue(reaches_goal(playfield_example_succeed, 100)) playfield_example_succeed = {
self.assertFalse(reaches_goal(playfield_example_fail, 100)) (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): def test_playfield_generation(self):
playfield, min_steps = generate_playfield(10, 10, (0, 0), Down, (9, 9), 10, 5) print()
# self.assertTrue(reaches_goal(playfield, min_steps)) 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__': if __name__ == '__main__':
unittest.main() unittest.main()