Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions examples/minesweeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
## Minesweeper

This example demonstrates a **step-based, autonomous Minesweeper model** implemented using the Mesa framework.

![minesweeper_screenshot.png](screenshot.png)

---

### Overview

Minesweeper is a classic grid-based puzzle game in which cells may contain hidden mines, and each revealed cell displays the number of neighboring mines.

In this example, Minesweeper is implemented as a **cellular process** that reveals itself over time without user input. The model uses local rules and neighborhood interactions to propagate information across the grid.

---

### Key features

- **Step-based dynamics:**
The model works step by step, revealing nearby cells as it expands.

- **One agent per cell:**
Each grid cell is represented by a `MineCell` agent that stores its revealed state and number of neighboring mines.

- **Frontier-based propagation:**
A *frontier* tracks which cells are actively being processed at each step, allowing wave-like expansion similar to flood-fill algorithms.

- **Automatic reseeding:**
When a revealed region finishes expanding, the model automatically selects a new unrevealed safe cell and continues the process.

- **Quantitative analysis:**
A `DataCollector` tracks the number of revealed cells and the size of the active frontier over time.

- **Matplotlib visualization:**
The grid and time-series plots are rendered using Mesa’s Matplotlib-based visualization components.

---

### Model behavior

At each step:

1. Cells in the current frontier are revealed.
2. If a revealed cell has zero neighboring mines, its neighbors are added to the next frontier.
3. Numbered cells form boundaries and do not propagate further.
4. When a region finishes expanding, a new safe cell is selected to start another reveal wave.
5. The model terminates when no unrevealed safe cells remain or a mine is revealed.

---

### Visualization

The Solara visualization includes:

- **Grid view:**
- Green squares: unrevealed cells
- Gray squares: revealed empty cells
- Numbers: neighboring mine counts
- Red X: mine (if revealed)

- **DataCollector plot:**
- **Revealed:** total number of revealed cells over time
- **Frontier:** number of active cells being processed per step

---

### Getting Started

#### Prerequisites

- Python 3.10 or higher
- Mesa 3.0 or higher
- NumPy

#### Running the Model

Navigate to the example directory and run:

```bash
solara run app.py
81 changes: 81 additions & 0 deletions examples/minesweeper/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from mesa.visualization import SolaraViz
from mesa.visualization.components.matplotlib_components import (
make_mpl_plot_component,
make_mpl_space_component,
)
from mesa.visualization.components.portrayal_components import AgentPortrayalStyle
from minesweeper.agents import MineCell
from minesweeper.model import MinesweeperModel


def agent_portrayal(agent: MineCell):
if not agent.revealed:
return AgentPortrayalStyle(
marker="s",
color="green",
size=80,
)

if agent.cell.mine:
return AgentPortrayalStyle(
marker="X",
color="red",
size=80,
)

if agent.neighbor_mines > 0:
return AgentPortrayalStyle(
marker=f"${agent.neighbor_mines}$",
color="black",
size=80,
)

return AgentPortrayalStyle(
marker="s",
color="lightgray",
size=80,
)


model_params = {
"seed": {"type": "InputText", "value": 42},
"mine_density": {
"type": "SliderFloat",
"value": 0.15,
"min": 0.05,
"max": 0.4,
"step": 0.05,
},
"width": {"type": "SliderInt", "value": 10, "min": 5, "max": 40},
"height": {"type": "SliderInt", "value": 10, "min": 5, "max": 40},
}


model = MinesweeperModel()


def post_process(ax):
ax.set_aspect("equal")
ax.set_xticks([])
ax.set_yticks([])


space = make_mpl_space_component(
agent_portrayal=agent_portrayal,
post_process=post_process,
draw_grid=True,
)

plot = make_mpl_plot_component(
{
"Revealed": "tab:blue",
"Frontier": "tab:orange",
}
)

page = SolaraViz(
model,
components=[space, plot],
model_params=model_params,
name="Minesweeper (Step-based)",
)
Empty file.
12 changes: 12 additions & 0 deletions examples/minesweeper/minesweeper/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from mesa.discrete_space import CellAgent


class MineCell(CellAgent):
def __init__(self, model, cell):
super().__init__(model)
self.cell = cell
self.revealed = False
self.neighbor_mines = 0

def step(self):
pass
91 changes: 91 additions & 0 deletions examples/minesweeper/minesweeper/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from mesa import Model
from mesa.datacollection import DataCollector
from mesa.discrete_space import OrthogonalMooreGrid, PropertyLayer

from .agents import MineCell


class MinesweeperModel(Model):
def __init__(self, width=10, height=10, mine_density=0.15, seed=42):
super().__init__(seed=seed)

self.game_over = False
self.frontier = set()

self.grid = OrthogonalMooreGrid(
(width, height),
torus=False,
random=self.random,
)

self.mine_layer = PropertyLayer(
"mine",
(width, height),
default_value=False,
dtype=bool,
)

self.mine_layer.data = self.rng.choice(
[True, False],
size=(width, height),
p=[mine_density, 1 - mine_density],
)

self.grid.add_property_layer(self.mine_layer)

MineCell.create_agents(
model=self,
n=width * height,
cell=self.grid.all_cells.cells,
)

for cell in self.grid.all_cells:
cell.agents[0].neighbor_mines = sum(n.mine for n in cell.neighborhood)

safe = [c for c in self.grid.all_cells if not c.mine]
self.frontier |= set(self.random.sample(safe, k=5))

self.datacollector = DataCollector(
model_reporters={
"Revealed": lambda m: sum(a.revealed for a in m.agents),
"Frontier": lambda m: len(m.frontier),
}
)

self.datacollector.collect(self)

def step(self):
if self.game_over:
return

if not self.frontier:
hidden_safe = [
c
for c in self.grid.all_cells
if not c.mine and not c.agents[0].revealed
]
if not hidden_safe:
return
self.frontier.add(self.random.choice(hidden_safe))

next_frontier = set()

for cell in self.frontier:
agent = cell.agents[0]

if agent.revealed:
continue

agent.revealed = True

if cell.mine:
self.game_over = True
return

if agent.neighbor_mines == 0:
for n in cell.neighborhood:
if not n.agents[0].revealed:
next_frontier.add(n)

self.frontier = next_frontier
self.datacollector.collect(self)
Binary file added examples/minesweeper/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.