#!/usr/bin/env python # 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 . # # ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' # # rollingstone.py # -------------------- # date created : Tue Aug 7 2012 # copyright : (C) 2012 Niels G. W. Serup # maintained by : Niels G. W. Serup """ 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 from robotgame.logic.direction import * import random class RollingStoneError(Exception): pass class WouldHitWall(RollingStoneError): 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): """ 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 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, max_steps): """ 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): 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): """ Generate a completable playfield. The generated playfield will have nstones stones nturns turns. A completable playfield will always be completable in either zero, one, or two steps. """ 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)] 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 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