a-robots-conundrum/robotgame/logic/rollingstone.py

223 lines
7.6 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 *
import robotgame.misc as misc
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.
This generator favours increasing the turn density the closer to the goal
it gets.
"""
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
min_width, min_height = min_height, min_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]
width, height = height, width
min_width, min_height = min_height, min_width
used_fields = _fields_from_turns(turns)
playfield = {}
del turns[-1]
for p, d in turns:
playfield[p] = d
for pos in misc.pick_random_elements(
list(set(itertools.product(range(width), range(height)))
- set(used_fields)), nstones):
playfield[pos] = Blocker
return playfield, len(used_fields) - 1
def generate_simple_unsolved_solvable_playfield(*args, **kwds):
"""
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(*args, **kwds)
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(*args, **kwds):
"""
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(
*args, **kwds)
missing_dir = list(set(all_directions) - set(directions))[0]
return playfield, steps, directions + \
[missing_dir] * (len(directions) / 3) + [Right]
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)