A few days ago someone posted a clever bubble-wrap “mini-game” on Discord. It was posted on a private server and my spouse ask me to forward that to them, but it worked with Discord’s spoiler-tag feature and if I waned to copy-paste it, the content I get was the content without spoiler-tags.

No problem, I’m an engineer. In seconds I crafted a quick python script that’s able to generate an `NxN` bubble-wrap for Discord.

``````print("Anyone want some bubble wrap?")
size = 8
print("\n".join(["||pop||" * size] * size))
``````

The first line is important because The first line is always contains the display name of the user who sent the message so it would look terrible without the initial “spam text”.

### We can do more

A few minutes later someone wrote

Popping this feels like playing Minesweeper without hitting a bomb xD

And, that’s it, it was something, something interesting. What do we need?

First of all, we need `math` and `random`, because we want to place mines on random positions and if we want to determine the number of mines based on a given percentage of the grid, we have to use one of them: `round`, `ceil`, `floor`.

Obviously, we need a function that can wrap a string in spoiler-tags (on Discord it’s `||hide me||`).

``````import math
import random

def hide(content):
return f'||{content}||'
``````

Next thing to build is the grid itself and place some mines on in.

``````BOMB_MARK = 'X'
size = 8
number_of_bombs = math.ceil(size*size * 0.2)
grid = " " * size * size

bombs = set()
while len(bombs) < number_of_bombs:

for i in bombs:
grid = grid[:i] + BOMB_MARK + grid[i+1:]
``````

That’s it, we have a `size * size` grid with 20% mines on it. We used `set` so while we generate random numbers, we can store them in the `bombs` set and we don’t have to care about duplications. It’s an easy way to generate `N` random numbers without duplications.

What next? We have to update all cells without a mine with a number that reflects how many adjacent mines it has.

So if our grid looks like this:

``````___
__B
B__
``````

We want to update all cells to look like this:

``````_11
12B
B21
``````

First real problem we just met, how to count them. Because we are using a single string to represent the while grid, the easiest way is to simply check all possible adjacent cells. For that (for readability) we can use a simple matrix-like array with all the possible cells relative to a given cell.

``````# (x, y)
search_matrix = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1),          ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)
]
``````

Now we can simply iterate through our grid and we can calculate position with:

• `x2 = index + x1`
• `y2 = index + y1 * size`
``````for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = [grid[i+p+(q*size)] == BOMB_MARK for (p, q) in search_matrix]
``````

It seems logical, but we will get a huge error when we are trying to reach indexes out of range, so we have to check if a given index is possible at all or not. We can introduce a simple function to check if it’s possible or not:

``````def possible(grid, i, p, q):
v = i+p+(q*size)

if v < 0 or v >= len(grid):
return False

return True
``````

With this new function, we can fix out problem. At the same time we can convert our results to numbers:

``````for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = len(list(filter(None, [
grid[i+p+(q*size)] == BOMB_MARK
for (p, q) in search_matrix
if possible(grid, i, p, q)])))
# Update the cell if it's not 0
if mines_around > 0:
grid = grid[:i] + str(mines_around) + grid[i+1:]
``````

It’s not obvious first, but this code still have a small bug. It counts adjacent cells through new-line, which is not the case we want, fortunately we can extend our `possible` function with a simple logic that checks if the calculated row is in the same row as our target row.

``````def possible(grid, i, p, q):
v = i+p+(q*size)

if v < 0 or v >= len(grid):
return False

return (v // size) == ((i + (q*size)) // size)
``````

And now, we are ready to render our grid.

``````for i in range(0, len(grid), size):
print("".join([hide(c) for c in grid[i:i+size]]))
``````

Characters have no fixed width, we have to find something with fixed width. What can we use? Emojis:

• `bomb` for a mine
• `white_large_square` for an empty cell
• `one -> eight` for numbers
``````# This function is here only because
# Hugo has no ability to disable emoji conversion per page
# and I did not find a better solution to write ": bomb :"
# without spaces and without auto-convert it to "💣"
def wrap(s):
return f':{s}:'

def to_emoji(char):
if char == BOMB_MARK:
return wrap("bomb")
if char == " ":
return wrap("white_large_square")
numbers = [
"one", "two", "three",
"four", "five", "six",
"seven", "eight"
]
return wrap(numbers[int(char)-1])
``````

We are ready to render the final grid, and now we add a `spam-text` line too, to be sure it’s not messed up when we post it on Discord.

``````print("Shall we play a game?")

for i in range(0, len(grid), size):
print("".join([hide(to_emoji(c)) for c in grid[i:i+size]]))
`````` ### Full Source

``````import math
import random

def hide(content):
return f'||{content}||'

def possible(grid, i, p, q):
v = i+p+(q*size)

if v < 0 or v >= len(grid):
return False

return (v // size) == ((i + (q*size)) // size)

# This function is here only because
# Hugo has no ability to disable emoji conversion per page
# and I did not find a better solution to write ": bomb :"
# without spaces and without auto-convert it to "💣"
def wrap(s):
return f':{s}:'

def to_emoji(char):
if char == BOMB_MARK:
return wrap("bomb")
if char == " ":
return wrap("white_large_square")
numbers = [
"one", "two", "three",
"four", "five", "six",
"seven", "eight"
]
return wrap(numbers[int(char)-1])

BOMB_MARK = 'X'
size = 8
number_of_bombs = math.ceil(size*size * 0.2)
grid = " " * size * size

bombs = set()
while len(bombs) < number_of_bombs:

for i in bombs:
grid = grid[:i] + BOMB_MARK + grid[i+1:]

# (x, y)
search_matrix = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1),          ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)
]

for i, c in enumerate(grid):
# if it's a bomb, we can skip
if c == BOMB_MARK:
continue
mines_around = len(list(filter(None, [
grid[i+p+(q*size)] == BOMB_MARK
for (p, q) in search_matrix
if possible(grid, i, p, q)])))
# Update the cell if it's not 0
if mines_around > 0:
grid = grid[:i] + str(mines_around) + grid[i+1:]

print("Shall we play a game?")

for i in range(0, len(grid), size):
print("".join([hide(to_emoji(c)) for c in grid[i:i+size]]))
``````