This is a simple implementation of the classic game Minesweeper using Nillion Blind Computation.
The Game is played in a 2D grid where the player must reveal all the cells that do not contain a mine. The player can reveal a cell by clicking on it. If the cell contains a mine, the game is over. If the cell does not contain a mine, the cell will reveal a number that indicates the number of mines in the neighboring cells. The player can also flag a cell by ⌥ + CLICK
on it. The player wins the game when all the cells that do not contain a mine are revealed.
The Board is a 24*24
grid with 75 mines
.
Before starting the game, make sure you have Metamask Flask installed and Nillion Snap Installed. First you need to start a local devnet. (more in the Get Started section)
- Connect you Nillion Key using the
Connect with Nillion
button. - Create a new game by clicking the
Create Game
button. - In this step you have to place 75 mines in the board. You can do this by clicking on the cells. You can also randomize the mines by clicking the
Shuffle
button on the bottom-right corner. - Once Mines are placed, click the
Create Game
button, this will store the game and give you a game id. Copy this id. - Go to home page and click the
Join Game
button. Paste the game id and clickJoin Game
. - Now you can start playing the game. Reveal the cells by clicking on them and flag the cells by
⌥ + CLICK
on them.
|
|
|
|
https://www.youtube.com/watch?v=sRLVX6E1iUQ
The game is implemented using Nillion Blind Computation. The game is divided into two parts, the frontend and the nada programs.
The frontend is inspired by the classic Minesweeper game from Windows XP. It uses 98.css
for the Windows XP look and feel. The game is implemented using Next.js and Tailwind CSS.
The nada programs has two parties:
Party1
: The party who places mines.Party2
: The party who minesweeps.
The program takes total of 152 inputs, divided into two parts:
- Mine Locations: Total of 75 mines divided as
x,y
values so total of 150 values. - User Move: The user move location as
location-0
andlocation-1
asx,y
value.
The main logic of Minesweeper game is to check the number adjacent mines and do the following:
- If the cell has a mine, the game is over.
- If the cell has no mine, reveal the cell and check the number of mines in the neighboring cells.
- If the cell has no mine and no neighboring mines, reveal all the neighboring cells.
- If the cell has no mine and has neighboring mines, reveal the cell and show the number of neighboring mines.
The nada program calculates the number of adjacent mines on the grid to that location and also checks if the location has a mine.
The program returns two outputs:
adjacent_mines
: The number of mines in the neighboring cells.game_over
: If the cell has a mine, the game is over.
Here is the Basic Logic for the nada program:
def is_mine(
mine_locations: list[list[SecretInteger]],
row: SecretInteger, col: SecretInteger
) -> SecretInteger:
result = Integer(-1)
for mine in mine_locations:
actual = (mine[0] * Integer(100) + mine[1])
given = (row * Integer(100) + col)
result = result * (actual - given)
res = is_equal(result, Integer(0))
return res
def count_adjacent_mines(
mine_locations: list[list[SecretInteger]],
row: SecretInteger,
col: SecretInteger
) -> SecretInteger:
count = Integer(1)
for i in range(8):
newRow = row + dx[i]
newCol = col + dy[i]
valid = is_valid(newRow, newCol)
mine = is_mine(mine_locations, newRow, newCol)
count = count + (Integer(1) * valid * mine)
return count
def make_move(
mine_locations: list[list[SecretInteger]],
row: SecretInteger,
col: SecretInteger
) -> list[Output]:
outputs: list[Output] = []
count = count_adjacent_mines(mine_locations, row, col)
game_over = is_mine(mine_locations, row, col)
outputs.append(Output(count, "adjacent_mines", Party("Party1")))
outputs.append(Output(game_over, "game_over", Party("Party1")))
return outputs
Full Code
from nada_dsl import *
BOARD_SIZE = 24
NUM_MINES = 75
valid = Integer(1)
invalid = Integer(0)
dx = [Integer(-1), Integer(-1), Integer(-1), Integer(0),
Integer(0), Integer(1), Integer(1), Integer(1)]
dy = [Integer(-1), Integer(0), Integer(1), Integer(-1),
Integer(1), Integer(-1), Integer(0), Integer(1)]
def is_equal(a: SecretInteger, b: SecretInteger) -> SecretInteger:
return (a > b).if_else(invalid, (a < b).if_else(invalid, valid))
def is_mine(
mine_locations: list[list[SecretInteger]],
row: SecretInteger, col: SecretInteger
) -> SecretInteger:
result = Integer(-1)
for mine in mine_locations:
actual = (mine[0] * Integer(100) + mine[1])
given = (row * Integer(100) + col)
result = result * (actual - given)
res = is_equal(result, Integer(0))
return res
def is_valid(row: SecretInteger, col: SecretInteger) -> SecretInteger:
valid_row_max = row <= Integer(BOARD_SIZE)
valid_row_min = row > Integer(0)
valid_col_max = col <= Integer(BOARD_SIZE)
valid_col_min = col > Integer(0)
cmp_row_max = valid_row_max.if_else(valid, invalid)
cmp_row_min = valid_row_min.if_else(valid, invalid)
cmp_col_max = valid_col_max.if_else(valid, invalid)
cmp_col_min = valid_col_min.if_else(valid, invalid)
res = cmp_row_max * cmp_row_min * cmp_col_max * cmp_col_min
return res
def count_adjacent_mines(
mine_locations: list[list[SecretInteger]],
row: SecretInteger,
col: SecretInteger
) -> SecretInteger:
count = Integer(1)
for i in range(8):
newRow = row + dx[i]
newCol = col + dy[i]
valid = is_valid(newRow, newCol)
mine = is_mine(mine_locations, newRow, newCol)
count = count + (Integer(1) * valid * mine)
return count
def make_move(
mine_locations: list[list[SecretInteger]],
row: SecretInteger,
col: SecretInteger
) -> list[Output]:
outputs: list[Output] = []
count = count_adjacent_mines(mine_locations, row, col)
game_over = is_mine(mine_locations, row, col)
outputs.append(Output(count, "adjacent_mines", Party("Party1")))
outputs.append(Output(game_over, "game_over", Party("Party1")))
return outputs
def nada_main():
party1 = Party(name="Party1")
party2 = Party(name="Party2")
mine_locations: list[list[SecretInteger]] = []
location: list[SecretInteger] = []
# Take Mine Locations from Party 1
for i in range(NUM_MINES):
mine_locations.append([])
mine_locations[i].append(SecretInteger(
Input(name="mine-x-" + str(i), party=party1)))
mine_locations[i].append(SecretInteger(
Input(name="mine-y-" + str(i), party=party1)))
# Take Input Location from Party 2
for i in range(2):
location.append(SecretInteger(
Input(name=f"location-{i}", party=party2)))
outputs: list[Output] = make_move(
mine_locations, row=location[0], col=location[1])
return outputs
- Frontend: Next.js, Tailwind CSS, shadcn
- Integration: wagmi, viem, nillion-sdk
- Programs: nada-dsl
The following repository is a turborepo and divided into the following:
apps/www
- The web application for the game.packages/programs
- The nillion programs for computation.
First install the dependencies by running the following:
pnpm install
Next, start the local devnet by going in the packages/programs
directory and running the following command:
pnpm create-venv && pnpm start-devnet
This will start a local devnet for the Nillion SDK. Once the devnet is started, you can go to the apps/www
directory and run the following:
pnpm dev
Make sure you have required environment variables in the
.env
file.
This will start the web application on http://localhost:3000
.