After seeing Pretty print dice faces from multiple rolls of multi-sided dices, I decided to make an infinite ASCII dice generator.
There was one requirement, it to follow a normal dice face.
And so I decided on making two classes, the Dice
and Ring
s.
Dice
outputs the ASCII dice, holds the rings, and tells the rings how many dots to display.
The rings on the other hand are just one ring of the die.
For example a normal 6 sided dice has two 'rings', the outer 8 and the inner 1.
So Ring
decide where to put the dots.
This method allows to easily extend the dice, but keep the same base layout for the dice. That is no matter the size we will always see the same first 6 faces. (If all 6 dot's go on the same ring)
After completing the program,
some bits to me look messy for example display
.
But I can't think of a way to make them easier to understand.
Or nicer to look at.
As far as I know it works with all sized dice. But I made the algorithms with odd sized dice in mind.
from math import ceil
class Ring:
def __init__(self, ring):
self.ring = ring
self.size = self._size(ring)
self.index = self._build_index(self.size)
def build(self, amount):
indexes = set(self.index(number) for number in range(amount))
return [index in indexes for index in range(self.size)]
def fill(self):
return [True for _ in range(self.size)]
@staticmethod
def _size(ring):
# Alternate way to think about this:
# max(ring ** 2 - (ring - 2) ** 2, 1)
return max((ring - 1) * 4, 1)
@staticmethod
def _build_index(size):
increment = size // 2
columns = size // 4
columns_increment = size // 8
def column_addition(column):
return (column * columns_increment + column // 2) % columns
if not columns:
column_addition = lambda x: x
def get_index(number):
addition = column_addition(number // 4)
addition += (number % 4 > 1) * columns
return (number * increment + addition) % size
return get_index
class AsciiDice:
def __init__(self, size, icons=' O'):
self._display = self.build_display(icons)
largest_ring = ceil(size ** 0.5)
start = 1 if largest_ring % 2 else 2
self._ring = largest_ring
self.rings = list(map(
Ring,
reversed(range(start, largest_ring + 1, 2))
))
# To be overwritten in a subclass.
# The amount is _always_ even.
# This is as the odd dot is added to the last result.
def _spread_amount(self, amount):
pass
def _build(self, amount):
if amount % 2:
spread = list(self._spread_amount(amount - 1))
spread[-1] += 1
else:
spread = list(self._spread_amount(amount))
for amount, dice in zip(spread, self.rings):
if amount == dice.size:
yield dice.fill()
else:
yield dice.build(amount)
def display(self, amount):
array = [[None for _ in range(self._ring)] for _ in range(self._ring)]
rings = self._build(amount)
for r, ring in enumerate(rings):
groups = len(ring) // 4
x = r
y = r
for i, value in enumerate(ring):
array[y][x] = value
if i < groups:
y += 1
elif i < groups * 2:
x += 1
elif i < groups * 3:
y -= 1
else:
x -= 1
case = ['+' + '-'*self._ring + '+']
return case + ['|' + self._display(i) + '|' for i in array] + case
@staticmethod
def build_display(icons):
def change_icon(value):
return icons[value]
def inner(array):
return ''.join(map(change_icon, array))
return inner
class SpreadoutDice(AsciiDice):
def _spread_amount(self, amount):
rings_amount = list(map(lambda x: x.size, self.rings[:-1]))
rings_slice = len(rings_amount)
spread = [0 for _ in range(rings_slice)]
while rings_slice:
each = amount // rings_slice
if each < 4:
rings_slice -= 1
continue
each = each // 4 * 4
for ring in range(rings_slice):
spread[ring] += each
amount -= each
if spread[ring] > rings_amount[ring]:
amount += spread[ring] - rings_amount[ring]
spread[ring] = rings_amount[ring]
rings_slice -= 1
spread[0] += amount
amount = 0
if spread[0] > rings_amount[0]:
amount += spread[0] - rings_amount[0]
spread[0] = rings_amount[0]
return spread + [amount]
class OuterDice(AsciiDice):
def _spread_amount(self, amount):
for ring in self.rings:
if amount > ring.size:
yield ring.size
elif amount > 0:
yield amount
else:
yield 0
amount -= ring.size
# From here down, doesn't really need reviewed.
class PrintSideBySide:
def __init__(self, amount_dice, dice_height, separator=' '):
self.amount = amount_dice
self.height = dice_height
self.clear_buff()
self.separator = separator
def print(self):
if all(self.buff):
print('\n'.join(self.buff))
def clear_buff(self):
self.buff = ['' for _ in range(self.height)]
self._size = 0
def add(self, dice):
for index, value in enumerate(dice):
self.buff[index] += self.separator + value
self._size += 1
if self._size == self.amount:
self.print()
self.clear_buff()
if __name__ == '__main__':
size = 3
buff = PrintSideBySide(9, size + 2)
dice = OuterDice(size ** 2)
for i in range(1, size ** 2 + 1):
buff.add(dice.display(i))
buff.print()
size = 5
buff = PrintSideBySide(5, size + 2)
dice = OuterDice(size ** 2)
for i in range(1, size ** 2 + 1):
buff.add(dice.display(i))
buff.print()
buff.clear_buff()
dice = SpreadoutDice(size ** 2)
for i in range(1, size ** 2 + 1):
buff.add(dice.display(i))
buff.print()
The output for size = 3
, from the above, is:
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ | | |O | |O | |O O| |O O| |O O| |O O| |OOO| |OOO| | O | | | | O | | | | O | |O O| |OOO| |O O| |OOO| | | | O| | O| |O O| |O O| |O O| |O O| |OOO| |OOO| +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
In the above size = 5
shows how the different classes change the pattern, but is too long to put here.