Finished rolling stone playfield generation.
This commit is contained in:
parent
2457f743cc
commit
6c050f954b
|
@ -1,3 +1,2 @@
|
||||||
*~
|
*~
|
||||||
*.pyc
|
*.py[co]
|
||||||
*.pyo
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _insert_stones(turns):
|
|
||||||
pass
|
|
||||||
|
|
||||||
turns = _find_min_turns(start_pos, start_direc)
|
if do_transpose:
|
||||||
if nturns is not None:
|
turns[:] = [((y, x), {
|
||||||
if len(turns) > nturns:
|
Down: Right,
|
||||||
raise RollingStoneError("Too few steps allocated.")
|
Right: Down,
|
||||||
_randomize_path(turns)
|
Up: Left,
|
||||||
_insert_stones()
|
}.get(d)) for ((x, y), d) in turns]
|
||||||
return playfield, 3
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue