Finished rolling stone playfield generation.
This commit is contained in:
parent
2457f743cc
commit
6c050f954b
|
@ -1,3 +1,2 @@
|
|||
*~
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.py[co]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
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:
|
||||
turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)]
|
||||
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))
|
||||
|
||||
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:
|
||||
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)
|
||||
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:
|
||||
if y0 == len(playfield):
|
||||
raise WouldHitWall
|
||||
turns = [((x, y), direc) for ((y, x), direc) in get_turns(y0, x0, y2, x2)]
|
||||
return turns
|
||||
t = -1 if direc == Left else 1
|
||||
for x in range(x0 + t, x2 + t, t):
|
||||
fields.append((x, y0))
|
||||
|
||||
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 _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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue