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
*.pyo
*.py[co]

View File

@ -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)

View File

@ -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
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))
def _randomize_path(turns):
pass
if do_transpose:
turns[:] = [((y, x), {
Down: Right,
Right: Down,
Up: Left,
}.get(d)) for ((x, y), d) in turns]
def _insert_stones(turns):
pass
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
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_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)

View File

@ -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()