| 1 | #!/usr/bin/python |
|---|
| 2 | # |
|---|
| 3 | # requires: argparse, networkx. Works on Linux. Might work on OSX. Probably doesn't work on Windows. Patches appreciated. |
|---|
| 4 | # |
|---|
| 5 | # Copyright 2010 Adrianna Pinska, Simon Cross |
|---|
| 6 | # This program is distributed under the terms of the GNU General Public License v3 |
|---|
| 7 | |
|---|
| 8 | import warnings |
|---|
| 9 | warnings.simplefilter("ignore", DeprecationWarning) |
|---|
| 10 | |
|---|
| 11 | import random |
|---|
| 12 | import os |
|---|
| 13 | from copy import deepcopy |
|---|
| 14 | |
|---|
| 15 | from argparse import ArgumentParser |
|---|
| 16 | from networkx import Graph, connected_components |
|---|
| 17 | |
|---|
| 18 | class Ball(object): |
|---|
| 19 | def __init__(self, ball=None): |
|---|
| 20 | self.ball = ball |
|---|
| 21 | |
|---|
| 22 | def __str__(self): |
|---|
| 23 | if self.ball is None: |
|---|
| 24 | return '-' |
|---|
| 25 | else: |
|---|
| 26 | return str(self.ball) |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | class Row(object): |
|---|
| 30 | def __init__(self, row_index): |
|---|
| 31 | self.even = not row_index % 2 |
|---|
| 32 | self.balls = [] |
|---|
| 33 | |
|---|
| 34 | def num_balls(self): |
|---|
| 35 | return 8 if self.even else 7 |
|---|
| 36 | |
|---|
| 37 | def __str__(self): |
|---|
| 38 | prefix = ' ' if not self.even else '' |
|---|
| 39 | return prefix + ' '.join([str(b) for b in self.balls]) |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | class RandomRow(Row): |
|---|
| 43 | def __init__(self, row_index, selection, symmetric): |
|---|
| 44 | super(RandomRow, self).__init__(row_index) |
|---|
| 45 | if symmetric: |
|---|
| 46 | startballs = [Ball(random.choice(selection)) for b in range(4)] |
|---|
| 47 | self.balls = startballs + list(reversed(deepcopy(startballs)[:self.num_balls()/2])) |
|---|
| 48 | else: |
|---|
| 49 | self.balls = [Ball(random.choice(selection)) for b in range(self.num_balls())] |
|---|
| 50 | |
|---|
| 51 | class BlankRow(Row): |
|---|
| 52 | def __init__(self, row_index): |
|---|
| 53 | super(BlankRow, self).__init__(row_index) |
|---|
| 54 | self.balls = [Ball()] * self.num_balls() |
|---|
| 55 | |
|---|
| 56 | class Level(object): |
|---|
| 57 | BALLS = range(8) |
|---|
| 58 | |
|---|
| 59 | ASYM, SYM_HORIZ, SYM_BOTH = range(3) |
|---|
| 60 | SYMMETRIES = { |
|---|
| 61 | "asym" : ASYM, |
|---|
| 62 | "horiz" : SYM_HORIZ, |
|---|
| 63 | "both" : SYM_BOTH, |
|---|
| 64 | } |
|---|
| 65 | |
|---|
| 66 | def __init__(self, fill_rows=None, colours=None, emptyspace=None, symmetry=None): |
|---|
| 67 | if fill_rows is None: |
|---|
| 68 | fill_rows = random.randint(6,10) |
|---|
| 69 | if colours is None: |
|---|
| 70 | colours = random.randint(1,8) |
|---|
| 71 | if emptyspace is None: |
|---|
| 72 | emptyspace = random.randint(1,4) |
|---|
| 73 | if symmetry is None: |
|---|
| 74 | symmetry = random.randint(0,2) |
|---|
| 75 | |
|---|
| 76 | self.rows = [] |
|---|
| 77 | |
|---|
| 78 | selection = random.sample(self.BALLS, colours) + [None] * emptyspace |
|---|
| 79 | symmetric_rows = symmetry > 0 |
|---|
| 80 | |
|---|
| 81 | if symmetry > 1: |
|---|
| 82 | if not fill_rows % 2: |
|---|
| 83 | fill_rows -= 1 |
|---|
| 84 | for row in range((fill_rows + 1)/2): |
|---|
| 85 | self.rows.append(RandomRow(row, selection, symmetric_rows)) |
|---|
| 86 | for rand_row in reversed(self.rows[:fill_rows/2]): |
|---|
| 87 | self.rows.append(deepcopy(rand_row)) |
|---|
| 88 | else: |
|---|
| 89 | for row in range(fill_rows): |
|---|
| 90 | self.rows.append(RandomRow(row, selection, symmetric_rows)) |
|---|
| 91 | |
|---|
| 92 | for row in range(fill_rows, 10): |
|---|
| 93 | self.rows.append(BlankRow(row)) |
|---|
| 94 | |
|---|
| 95 | def valid(self): |
|---|
| 96 | g = Graph() |
|---|
| 97 | |
|---|
| 98 | def add_edge(b1, b2): |
|---|
| 99 | if b1.ball is not None and b2.ball is not None: |
|---|
| 100 | g.add_edge(b1, b2) |
|---|
| 101 | |
|---|
| 102 | # add all balls and horizontal edges |
|---|
| 103 | for row in self.rows: |
|---|
| 104 | for ball in row.balls: |
|---|
| 105 | if ball.ball is not None: |
|---|
| 106 | g.add_node(ball) |
|---|
| 107 | for b1, b2 in zip(row.balls[:-1], row.balls[1:]): |
|---|
| 108 | add_edge(b1, b2) |
|---|
| 109 | |
|---|
| 110 | # add inter-row connections |
|---|
| 111 | for row, above, below in zip(self.rows[1::2], self.rows[::2], self.rows[2::2] + [BlankRow(0)]): |
|---|
| 112 | for ball, above_left, above_right, below_left, below_right in zip(row.balls, above.balls[:-1], above.balls[1:], below.balls[:-1], below.balls[1:]): |
|---|
| 113 | add_edge(ball, above_left) |
|---|
| 114 | add_edge(ball, above_right) |
|---|
| 115 | add_edge(ball, below_left) |
|---|
| 116 | add_edge(ball, below_right) |
|---|
| 117 | |
|---|
| 118 | top_row = set(self.rows[0].balls) |
|---|
| 119 | valid = True |
|---|
| 120 | for comp in connected_components(g): |
|---|
| 121 | if set(comp).isdisjoint(top_row): |
|---|
| 122 | valid = False |
|---|
| 123 | break |
|---|
| 124 | |
|---|
| 125 | return valid |
|---|
| 126 | |
|---|
| 127 | def __str__(self): |
|---|
| 128 | return '\n'.join([str(r) for r in self.rows]) |
|---|
| 129 | |
|---|
| 130 | |
|---|
| 131 | class LevelSet(object): |
|---|
| 132 | def __init__(self, num_levels, **kwargs): |
|---|
| 133 | l = 0 |
|---|
| 134 | self.levels = [] |
|---|
| 135 | |
|---|
| 136 | while l < num_levels: |
|---|
| 137 | level = Level(**kwargs) |
|---|
| 138 | if level.valid(): |
|---|
| 139 | self.levels.append(level) |
|---|
| 140 | l += 1 |
|---|
| 141 | |
|---|
| 142 | def __str__(self): |
|---|
| 143 | return '\n\n'.join([str(l) for l in self.levels]) |
|---|
| 144 | |
|---|
| 145 | if __name__ == '__main__': |
|---|
| 146 | parser = ArgumentParser() |
|---|
| 147 | parser.add_argument('-f', '--filename', dest='filename', type=str, default="%s/.frozen-bubble/levels/foreverbubble" % os.getenv('HOME'), metavar='FILE', help='levelset save location (default: %(default)s)') |
|---|
| 148 | parser.add_argument('-n', '--number', dest='number', type=int, default=20, metavar='NUMBER', help='number of levels to generate (default: %(default)s)') |
|---|
| 149 | |
|---|
| 150 | parser.add_argument('-r', '--rows', dest='fill_rows', type=int, choices=xrange(1,11), metavar='ROWS', help='number of rows to generate per level (permitted values: %(choices)s; default: random)') |
|---|
| 151 | parser.add_argument('-c', '--colours', dest='colours', type=int, choices=xrange(1,9), metavar='COLOURS', help='number of colours to use per level (permitted values: %(choices)s; default: random)') |
|---|
| 152 | parser.add_argument('-e', '--empty-space', dest='emptyspace', type=int, choices=xrange(0,4), metavar='SPACE', help='amount of empty space on each level (permitted values: %(choices)s; default: random)') |
|---|
| 153 | parser.add_argument('-s', '--symmetry', dest='symmetry', type=str, choices=Level.SYMMETRIES.keys(), metavar='SYMMETRY', help='symmetry of each level (permitted values: %(choices)s; default: random)') |
|---|
| 154 | args = parser.parse_args() |
|---|
| 155 | |
|---|
| 156 | symmetry = Level.SYMMETRIES[args.symmetry] if args.symmetry else None |
|---|
| 157 | |
|---|
| 158 | levelset = LevelSet(args.number, fill_rows=args.fill_rows, colours=args.colours, emptyspace=args.emptyspace, symmetry=symmetry) |
|---|
| 159 | |
|---|
| 160 | levelsetfile = open(args.filename, "w") |
|---|
| 161 | levelsetfile.write(str(levelset)) |
|---|
| 162 | levelsetfile.close() |
|---|