Updated filenames and file contents to fit the title of the game. Made proper readme.

This commit is contained in:
2012-10-12 13:50:02 +02:00
parent 9943e87f4a
commit 9b7d4279ce
35 changed files with 1398 additions and 273 deletions

View File

View File

@@ -0,0 +1,141 @@
#!/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 <http://www.gnu.org/licenses/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
# colourboxes.py
# --------------------
# date created : Wed Aug 8 2012
# copyright : (C) 2012 Niels G. W. Serup
# maintained by : Niels G. W. Serup <ngws@metanohi.name>
"""
Colour boxes.
"""
from __future__ import print_function
import random
import itertools
def generate_colour_boxes(nwells, nboxes):
"""
Generate colour boxes that can be used to make all wells white. None of the
generated colour boxes are white.
Arguments:
nwells -- number of wells
nboxes -- maximum number of boxes needed to make all wells white.
Return [[(r, g, b)]]
where r : 0|1, g : 0|1, b : 0|1
"""
nbits = nwells * 3
data = [[0 for _ in range(nboxes)] for _ in range(nbits)]
def insert_1(x):
t = random.randrange(0, nboxes)
x0, x1 = _get_oxs(x)
for y in range(t, nboxes) + range(0, t):
if data[x][y] == 0 and not (data[x0][y] == 1 and data[x1][y] == 1):
data[x][y] = 1
break
else:
raise Exception("Cannot maintain no 111s invariant.")
def insert_two_1(x):
t = random.randrange(0, nboxes)
x0, x1 = _get_oxs(x)
if len(list(filter(lambda y: data[x][y] == 0 and not (data[x0][y] == 1 and data[x1][y] == 1),
range(nboxes)))) < 2:
return
for _ in range(2):
for y in range(t, nboxes) + range(0, t):
if data[x][y] == 0 and not (data[x0][y] == 1 and data[x1][y] == 1):
data[x][y] = 1
break
for x in range(len(data)):
insert_1(x)
for x in range(len(data)):
for _ in range(random.randrange(0, (nboxes + 1) / 2)):
insert_two_1(x)
boxes = []
for y in range(nboxes):
box = []
boxes.append(box)
for x in range(0, nbits, 3):
r = data[x][y]
g = data[x + 1][y]
b = data[x + 2][y]
box.append((r, g, b))
return boxes
def _get_oxs(x):
return (x + 1, x + 2) if x % 3 == 0 \
else (x - 1, x + 1) if x % 3 == 1 \
else (x - 2, x - 1)
def generate_random_box(nwells, min_nonblacks=0):
"""
Generate a box that triggers nwells wells, with random colors except white
(111).
Arguments:
min_nonblacks -- minimum number of well colours in a box required not to be
black.
"""
def gen_wc():
wc = [random.choice((0, 1)) for i in range(3)]
if all(b == 1 for b in wc):
wc[random.randrange(3)] = 0
return wc
def gen_wc_nonblack():
wc = gen_wc()
if all(b == 0 for b in wc):
wc[random.randrange(3)] = 1
return wc
colours = [tuple(gen_wc()) for _ in range(nwells)]
nonblack = lambda t: any(n == 1 for n in t)
missing_nonblacks = min_nonblacks - len(list(filter(nonblack, colours)))
i = 0
while missing_nonblacks > 0:
if not nonblack(colours[i]):
colours[i] = gen_wc_nonblack()
missing_nonblacks -= 1
i += 1
return colours
def get_colours(boxes):
colours = []
for i in range(len(boxes[0])):
r, g, b = boxes[0][i]
for j in range(1, len(boxes)):
r1, g1, b1 = boxes[j][i]
r ^= r1
g ^= g1
b ^= b1
colours.append((r, g, b))
return colours
def makes_all_wells_white(boxes):
"""
Determine if the boxes make all wells white when XOR'ed together.
"""
return all(c == 1 for c in itertools.chain(*get_colours(boxes)))

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python
# 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 <http://www.gnu.org/licenses/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
# direction.py
# --------------------
# date created : Tue Aug 7 2012
# copyright : (C) 2012 Niels G. W. Serup
# maintained by : Niels G. W. Serup <ngws@metanohi.name>
"""Directions."""
class Direction(object):
@staticmethod
def next_pos(pos):
raise NotImplementedError
@staticmethod
def to_str():
raise NotImplementedError
@staticmethod
def from_sakse(p):
return {(0, -1): Up,
(0, 1): Down,
(-1, 0): Left,
(1, 0): Right}[p]
class Up(Direction):
@staticmethod
def next_pos(pos):
x, y = pos
return x, y - 1
@staticmethod
def to_str():
return 'up'
class Right(Direction):
@staticmethod
def next_pos(pos):
x, y = pos
return x + 1, y
@staticmethod
def to_str():
return 'right'
class Down(Direction):
@staticmethod
def next_pos(pos):
x, y = pos
return x, y + 1
@staticmethod
def to_str():
return 'down'
class Left(Direction):
@staticmethod
def next_pos(pos):
x, y = pos
return x - 1, y
@staticmethod
def to_str():
return 'left'
all_directions = [Up, Right, Down, Left]
_sp = lambda n: lambda d: all_directions[(all_directions.index(d) + n) % 4]
succ, pred = _sp(1), _sp(-1)
isDirection = all_directions.__contains__

View File

@@ -0,0 +1,218 @@
#!/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 <http://www.gnu.org/licenses/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
# lasermirror.py
# --------------------
# date created : Tue Aug 7 2012
# copyright : (C) 2012 Niels G. W. Serup
# maintained by : Niels G. W. Serup <ngws@metanohi.name>
"""
Management of lasers in rooms of mirrors and targets.
"""
from __future__ import print_function
import math
import random
import itertools
from arobotsconundrum.logic.direction import *
import arobotsconundrum.logic.rollingstone as rstone
from arobotsconundrum.logic.rollingstone import Blocker
import arobotsconundrum.misc as misc
class MirrorLeft(object):
pass
class MirrorRight(object):
pass
class Lever(object):
pass
class Target(object):
pass
class Source(object):
def __init__(self, direction):
self.__dict__.update(locals())
def generate_simple_playfield(nmirrors):
"""
Generate a completable 17x17 playfield where:
* there are four laser sources, one in each corner
+ the one in the upper left corner (0, 0) starts in (0, -1) heading down
+ the one in the upper right corner (16, 0) starts in (17, 0), heading left
+ the one in the lower right corner (16, 16) starts in (16, 17), heading up
+ the one in the lower left corner (0, 16) starts in (-1, 16), heading right
* there are four laser targets
* there are nmirrors mirrors
* there are nmirrors levers
* all levers are at the wall
Return playfield : {(x, y):
Target | MirrorLeft | MirrorRight | rstone.Blocker | Lever}
"""
width, height = 17, 17
playfield = {(0, 0): Source(Down),
(width - 1, 0): Source(Left),
(width - 1, height - 1): Source(Up),
(0, height - 1): Source(Right),
(6, 6): Target,
(10, 6): Target,
(6, 10): Target,
(10, 10): Target,
(7, 7): rstone.Blocker,
(7, 8): rstone.Blocker,
(7, 9): rstone.Blocker,
(8, 7): rstone.Blocker,
(8, 8): rstone.Blocker,
(8, 9): rstone.Blocker,
(9, 7): rstone.Blocker,
(9, 8): rstone.Blocker,
(9, 9): rstone.Blocker,
}
succs = lambda d: d
source_direc = Up
nlevers = nmirrors
for missing in range(4, 0, -1):
nm = nmirrors / missing
nmirrors -= nm
stone_playfield, _ = rstone.generate_simple_playfield(
7, 7, nm, 0, False, False)
for pos, direc in stone_playfield.items():
playfield[_adjust(source_direc, width - 1, height - 1, *pos)] \
= random.choice((MirrorLeft, MirrorRight))
succs = (lambda s: lambda d: succ(s(d)))(succs)
source_direc = succ(source_direc)
occup = set(playfield.keys())
is_empty = lambda x, y: (x, y) not in occup
ok_a = lambda y: is_empty(1, y)
ok_b = lambda y: is_empty(width - 2, y)
ok_c = lambda x: is_empty(x, 1)
ok_d = lambda x: is_empty(x, height - 2)
no_block = lambda x, y: \
all((ok_a(y) if x == 0 else True,
ok_b(y) if x == width - 1 else True,
ok_c(x) if y == 0 else True,
ok_d(x) if y == height - 1 else True))
emptys = set([(0, y) for y in filter(ok_a, range(height))]
+ [(width - 1, y) for y in filter(ok_b, range(height))]
+ [(x, 0) for x in filter(ok_c, range(width))]
+ [(x, height - 1) for x in filter(ok_d, range(width))]) - occup
emptys_full = set(itertools.product(range(width), range(height))) - occup
emptys = list(emptys)
random.shuffle(emptys)
emptys = set(emptys)
is_empty = lambda x, y: (x, y) in emptys_full
levers = []
for _ in range(nlevers):
while True:
pos = next(iter(emptys))
emptys.remove(pos)
emptys_full.remove(pos)
if no_block(*pos):
playfield[pos] = Lever
if not all(no_block(*pos) for pos in levers):
del playfield[pos]
else:
levers.append(pos)
break
return playfield
def _adjust(source_direc, w, h, x, y):
return {
Up: lambda x, y: (x, y),
Right: lambda x, y: (w - y, x),
Down: lambda x, y: (w - x, h - y),
Left: lambda x, y: (y, h - x),
}[source_direc](x, y)
def generate_lasers(playfield):
"""
Generate laser paths.
Return [((x, y), direction), ...]
"""
width, height = 17, 17
sources = ((pos, obj.direction) for pos, obj
in filter(lambda posobj: isinstance(posobj[1], Source),
playfield.items()))
lasers, lasers_flat = [], set()
def add(start, end):
t = (min(start, end), max(start, end))
if not t in lasers_flat:
laser.append(t)
lasers_flat.add(t)
for start, direc in sources:
end = start
laser = []
lasers.append(laser)
while True:
cur = playfield.get(end)
if cur is Target:
add(start, end)
break
if cur is Blocker:
add(start, end)
break
if cur in (MirrorLeft, MirrorRight):
if (start, end) in ((start, end) for (start, end), direc in lasers_flat):
break
add(start, end)
direc = _mirror_new_direc(cur, direc)
start = end
new_end = direc.next_pos(end)
if new_end[0] < 0 or new_end[1] < 0 or \
new_end[0] >= width or new_end[1] >= height:
add(start, new_end)
break
end = new_end
return lasers
def _mirror_new_direc(mirror_type, old_direc):
return {Down: (Left, Right),
Left: (Down, Up),
Up: (Right, Left),
Right: (Up, Down)}[old_direc][
0 if mirror_type is MirrorLeft else 1]
def print_playfield(playfield, width, height, hide_directions=False):
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 rstone.Blocker \
else 'x' if val is Mirror \
else 'L' if val is Lever \
else 'T' if val is Target else 'N'
print('\n'.join(''.join(line) for line in text))

View File

@@ -0,0 +1,223 @@
#!/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 <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 <ngws@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 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)

View File

@@ -0,0 +1,144 @@
#!/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 <http://www.gnu.org/licenses/>.
#
# ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' '
#
# teleportermap.py
# --------------------
# date created : Mon Aug 13 2012
# copyright : (C) 2012 Niels G. W. Serup
# maintained by : Niels G. W. Serup <ngws@metanohi.name>
"""
Logic for a map with invisible teleporters.
"""
from __future__ import print_function
import math
import random
import itertools
from arobotsconundrum.logic.direction import *
import arobotsconundrum.misc as misc
class Empty(object):
pass
class Forbidden(object):
pass
class StrictlyForbidden(object):
pass
class Visited(object):
pass
def generate_teleporter_map(width, height):
m = [[Empty for _ in range(height)]
for _ in range(width)]
def _get(p):
if p[0] < 0 or p[0] >= width or p[1] < 0 or p[1] >= height:
return StrictlyForbidden
return m[p[0]][p[1]]
def _set(p, val):
try:
m[p[0]][p[1]] = val
except IndexError:
pass
def insert_random_point_on_line(x):
t = random.randrange(height)
for y in itertools.chain(xrange(t), xrange(t + 1, height)):
_set((x, y), Forbidden)
_set((x, t), Visited)
return (x, t)
pos = insert_random_point_on_line(width - 1)
while True:
while True:
if pos[0] == 1 and random.randint(0, height) == 0:
break
t = random.randrange(4)
found = None
alldirs = all_directions
if random.choice((True, False)):
alldirs.reverse()
for direc in alldirs[t:] + alldirs[:t]:
npos = direc.next_pos(pos)
if found is None and npos[0] != 0 and _get(npos) is Empty and (
(direc is Right and
len(filter(lambda c: c is Empty, m[pos[0]][:pos[1]] if
opos[1] > pos[1]
else m[pos[0]][pos[1] + 1:])) >= 2)
or direc is not Right):
found = npos
_set(npos, Visited)
else:
if _get(npos) is Empty:
_set(npos, Forbidden)
if found is None:
break
opos = pos
pos = found
if pos[0] == 1:
break
_set(pos, StrictlyForbidden)
for direc in all_directions:
npos = direc.next_pos(pos)
if _get(npos) is Visited:
pos = npos
break
for direc in all_directions:
npos = direc.next_pos(pos)
if _get(npos) is Forbidden:
occup_direc = succ(succ(direc))
if all(ndirec is occup_direc
or _get(ndirec.next_pos(npos)) in (Forbidden, Empty)
for ndirec in all_directions):
_set(npos, Empty)
_set((0, pos[1]), Visited)
return m
def generate_teleporter_map2(width, height):
tmap = generate_teleporter_map(width, height)
res = set()
for y in range(len(tmap[0])):
for x in range(len(tmap)):
if tmap[x][y] is Visited:
res.add((x, y))
return res
def generate_teleporter_map3(width, height):
return set(itertools.product(range(width), range(height))) \
- generate_teleporter_map2(width, height)
def print_map(tmap):
for y in range(len(tmap[0])):
for x in range(len(tmap)):
c = tmap[x][y]
print({Empty: '%',
Forbidden: '#',
StrictlyForbidden: '!',
Visited: '·'}[c], end='')
print()