217 lines
7.5 KiB
Python
217 lines
7.5 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# This file is part of ROBOTGAME
|
|
#
|
|
# ROBOTGAME is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# ROBOTGAME is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# ROBOTGAME. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
|
|
#
|
|
# rollingstone.py
|
|
# --------------------
|
|
# date created : Tue Aug 7 2012
|
|
# copyright : (C) 2012 Niels G. W. Serup
|
|
# maintained by : Niels G. W. Serup <ns@metanohi.name>
|
|
|
|
"""
|
|
Logic for a rolling stone on a playfield of movement-stopping stones and
|
|
direction-changing turns. Also has a pseudo-random playfield generator.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import math
|
|
import random
|
|
import itertools
|
|
from robotgame.logic.direction import *
|
|
|
|
class Blocker(object):
|
|
pass
|
|
|
|
def step(playfield, width, height, old_pos, direc):
|
|
"""
|
|
Return a new (position, direction) tuple based on the location on the
|
|
playfield.
|
|
"""
|
|
pos = direc.next_pos(old_pos)
|
|
x, y = pos
|
|
if playfield.get(pos) is Blocker 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
|
|
|
|
def reaches_goal(playfield, width, height, max_steps, start_pos, goal_pos):
|
|
"""
|
|
Determine if the rolling stone reaches the goal within max_steps steps.
|
|
|
|
playfield[start_pos] must contain either a Turn(Down) or a Turn(Right)
|
|
object, or the rolling stone will not roll.
|
|
"""
|
|
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 generate_simple_playfield(width, height, nturns, nstones,
|
|
do_transpose=None, start_inside=True):
|
|
"""
|
|
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
|
|
|
|
Return (playfield : {(x, y): Direction | Blocker},
|
|
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.
|
|
"""
|
|
|
|
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)
|
|
|
|
if do_transpose is None:
|
|
do_transpose = random.choice((True, False))
|
|
if do_transpose:
|
|
width, height = height, width
|
|
|
|
turns = [((0, 0), None)]
|
|
stones = []
|
|
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:
|
|
turns[-1] = ((width - 1, turns[-1][0][1]), Down)
|
|
break
|
|
else:
|
|
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 not start_inside:
|
|
del turns[0]
|
|
|
|
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 = {}
|
|
del turns[-1]
|
|
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] = Blocker
|
|
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, its number of
|
|
steps, and a list of the direction objects.
|
|
"""
|
|
playfield, steps = generate_simple_playfield(width, height, nturns, nstones)
|
|
new_playfield, directions = {}, []
|
|
for pos, val in playfield.items():
|
|
if val is Blocker:
|
|
new_playfield[pos] = val
|
|
else:
|
|
directions.append(val)
|
|
return new_playfield, steps, 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, steps, directions = generate_simple_unsolved_solvable_playfield(
|
|
width, height, nturns, nstones)
|
|
missing_dir = list(set(all_directions) - set(directions))[0]
|
|
return playfield, steps, 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 is Blocker 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)
|