#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of A Robot's Conundrum. # # A Robot's Conundrum 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. # # A Robot's Conundrum 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 # A Robot's Conundrum. 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 import math import random import itertools from arobotsconundrum.logic.direction import * import arobotsconundrum.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)