1/* Lists/Minesweeper
2 * #Keywords: GtkGridView, GListModel, game
3 *
4 * This demo shows how to develop a user interface for small game using a
5 * grid view.
6 *
7 * It demonstrates how to use the activate signal and single-press behavior
8 * to implement rather different interaction behavior to a typical list.
9 */
10
11#include <glib/gi18n.h>
12#include <gtk/gtk.h>
13
14/*** The cell object ***/
15
16/* Create an object that holds the data for a cell in the game */
17typedef struct _SweeperCell SweeperCell;
18struct _SweeperCell
19{
20 GObject parent_instance;
21
22 gboolean is_mine;
23 gboolean is_visible;
24 guint neighbor_mines;
25};
26
27enum {
28 CELL_PROP_0,
29 CELL_PROP_LABEL,
30
31 N_CELL_PROPS
32};
33
34#define SWEEPER_TYPE_CELL (sweeper_cell_get_type ())
35G_DECLARE_FINAL_TYPE (SweeperCell, sweeper_cell, SWEEPER, CELL, GObject);
36
37G_DEFINE_TYPE (SweeperCell, sweeper_cell, G_TYPE_OBJECT);
38static GParamSpec *cell_properties[N_CELL_PROPS] = { NULL, };
39
40static const char *
41sweeper_cell_get_label (SweeperCell *self)
42{
43 static const char *minecount_labels[10] = { "", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
44
45 if (!self->is_visible)
46 return "?";
47
48 if (self->is_mine)
49 return "💣";
50
51 return minecount_labels[self->neighbor_mines];
52}
53
54static void
55sweeper_cell_get_property (GObject *object,
56 guint property_id,
57 GValue *value,
58 GParamSpec *pspec)
59{
60 SweeperCell *self = SWEEPER_CELL (ptr: object);
61
62 switch (property_id)
63 {
64 case CELL_PROP_LABEL:
65 g_value_set_string (value, v_string: sweeper_cell_get_label (self));
66 break;
67
68 default:
69 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
70 break;
71 }
72}
73
74static void
75sweeper_cell_class_init (SweeperCellClass *klass)
76{
77 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
78
79 gobject_class->get_property = sweeper_cell_get_property;
80
81 cell_properties[CELL_PROP_LABEL] =
82 g_param_spec_string (name: "label",
83 nick: "label",
84 blurb: "label to display for this row",
85 NULL,
86 flags: G_PARAM_READABLE);
87
88 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_CELL_PROPS, pspecs: cell_properties);
89}
90
91static void
92sweeper_cell_init (SweeperCell *self)
93{
94}
95
96static void
97sweeper_cell_reveal (SweeperCell *self)
98{
99 if (self->is_visible)
100 return;
101
102 self->is_visible = TRUE;
103
104 g_object_notify_by_pspec (G_OBJECT (self), pspec: cell_properties[CELL_PROP_LABEL]);
105}
106
107static SweeperCell *
108sweeper_cell_new (void)
109{
110 return g_object_new (SWEEPER_TYPE_CELL, NULL);
111}
112
113/*** The board object ***/
114
115/* Create an object that holds the data for the game */
116typedef struct _SweeperGame SweeperGame;
117struct _SweeperGame
118{
119 GObject parent_instance;
120
121 GPtrArray *cells;
122 guint width;
123 guint height;
124 gboolean playing;
125 gboolean win;
126};
127
128enum {
129 GAME_PROP_0,
130 GAME_PROP_HEIGHT,
131 GAME_PROP_PLAYING,
132 GAME_PROP_WIDTH,
133 GAME_PROP_WIN,
134
135 N_GAME_PROPS
136};
137
138#define SWEEPER_TYPE_GAME (sweeper_game_get_type ())
139G_DECLARE_FINAL_TYPE (SweeperGame, sweeper_game, SWEEPER, GAME, GObject);
140
141static GType
142sweeper_game_list_model_get_item_type (GListModel *model)
143{
144 return SWEEPER_TYPE_GAME;
145}
146
147static guint
148sweeper_game_list_model_get_n_items (GListModel *model)
149{
150 SweeperGame *self = SWEEPER_GAME (ptr: model);
151
152 return self->width * self->height;
153}
154
155static gpointer
156sweeper_game_list_model_get_item (GListModel *model,
157 guint position)
158{
159 SweeperGame *self = SWEEPER_GAME (ptr: model);
160
161 return g_object_ref (g_ptr_array_index (self->cells, position));
162}
163
164static void
165sweeper_game_list_model_init (GListModelInterface *iface)
166{
167 iface->get_item_type = sweeper_game_list_model_get_item_type;
168 iface->get_n_items = sweeper_game_list_model_get_n_items;
169 iface->get_item = sweeper_game_list_model_get_item;
170}
171
172G_DEFINE_TYPE_WITH_CODE (SweeperGame, sweeper_game, G_TYPE_OBJECT,
173 G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, sweeper_game_list_model_init))
174
175static GParamSpec *game_properties[N_GAME_PROPS] = { NULL, };
176
177static void
178sweeper_game_dispose (GObject *object)
179{
180 SweeperGame *self = SWEEPER_GAME (ptr: object);
181
182 g_clear_pointer (&self->cells, g_ptr_array_unref);
183
184 G_OBJECT_CLASS (sweeper_game_parent_class)->dispose (object);
185}
186
187static void
188sweeper_game_get_property (GObject *object,
189 guint property_id,
190 GValue *value,
191 GParamSpec *pspec)
192{
193 SweeperGame *self = SWEEPER_GAME (ptr: object);
194
195 switch (property_id)
196 {
197 case GAME_PROP_HEIGHT:
198 g_value_set_uint (value, v_uint: self->height);
199 break;
200
201 case GAME_PROP_PLAYING:
202 g_value_set_boolean (value, v_boolean: self->playing);
203 break;
204
205 case GAME_PROP_WIDTH:
206 g_value_set_uint (value, v_uint: self->width);
207 break;
208
209 case GAME_PROP_WIN:
210 g_value_set_boolean (value, v_boolean: self->win);
211 break;
212
213 default:
214 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
215 break;
216 }
217}
218
219static void
220sweeper_game_class_init (SweeperGameClass *klass)
221{
222 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
223
224 gobject_class->dispose = sweeper_game_dispose;
225 gobject_class->get_property = sweeper_game_get_property;
226
227 game_properties[GAME_PROP_HEIGHT] =
228 g_param_spec_uint (name: "height",
229 nick: "height",
230 blurb: "height of the game grid",
231 minimum: 1, G_MAXUINT, default_value: 8,
232 flags: G_PARAM_READABLE);
233
234 game_properties[GAME_PROP_PLAYING] =
235 g_param_spec_boolean (name: "playing",
236 nick: "playing",
237 blurb: "if the game is still going on",
238 FALSE,
239 flags: G_PARAM_READABLE);
240
241 game_properties[GAME_PROP_WIDTH] =
242 g_param_spec_uint (name: "width",
243 nick: "width",
244 blurb: "width of the game grid",
245 minimum: 1, G_MAXUINT, default_value: 8,
246 flags: G_PARAM_READABLE);
247
248 game_properties[GAME_PROP_WIN] =
249 g_param_spec_boolean (name: "win",
250 nick: "win",
251 blurb: "if the game was won",
252 FALSE,
253 flags: G_PARAM_READABLE);
254
255 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_GAME_PROPS, pspecs: game_properties);
256}
257
258static void
259sweeper_game_reset_board (SweeperGame *self,
260 guint width,
261 guint height)
262{
263 guint i;
264
265 g_ptr_array_set_size (array: self->cells, length: 0);
266
267 for (i = 0; i < width * height; i++)
268 {
269 g_ptr_array_add (array: self->cells, data: sweeper_cell_new ());
270 }
271
272 if (self->width != width)
273 {
274 self->width = width;
275 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_WIDTH]);
276 }
277 if (self->height != height)
278 {
279 self->height = height;
280 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_HEIGHT]);
281 }
282 if (!self->playing)
283 {
284 self->playing = TRUE;
285 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_PLAYING]);
286 }
287 if (self->win)
288 {
289 self->win = FALSE;
290 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_WIN]);
291 }
292}
293
294static void
295sweeper_game_place_mines (SweeperGame *self,
296 guint n_mines)
297{
298 guint i;
299
300 for (i = 0; i < n_mines; i++)
301 {
302 SweeperCell *cell;
303
304 do {
305 cell = g_ptr_array_index (self->cells, g_random_int_range (0, self->cells->len));
306 } while (cell->is_mine);
307
308 cell->is_mine = TRUE;
309 }
310}
311
312static SweeperCell *
313get_cell (SweeperGame *self,
314 guint x,
315 guint y)
316{
317 return g_ptr_array_index (self->cells, y * self->width + x);
318}
319
320static void
321sweeper_game_count_neighbor_mines (SweeperGame *self,
322 guint width,
323 guint height)
324{
325 guint x, y, x2, y2;
326
327 for (y = 0; y < height; y++)
328 {
329 for (x = 0; x < width; x++)
330 {
331 SweeperCell *cell = get_cell (self, x, y);
332
333 for (y2 = MAX (1, y) - 1; y2 < MIN (height, y + 2); y2++)
334 {
335 for (x2 = MAX (1, x) - 1; x2 < MIN (width, x + 2); x2++)
336 {
337 SweeperCell *other = get_cell (self, x: x2, y: y2);
338
339 if (other->is_mine)
340 cell->neighbor_mines++;
341 }
342 }
343 }
344 }
345}
346
347static void
348sweeper_game_new_game (SweeperGame *self,
349 guint width,
350 guint height,
351 guint n_mines)
352{
353 guint n_items_before;
354
355 g_return_if_fail (n_mines <= width * height);
356
357 n_items_before = self->width * self->height;
358
359 g_object_freeze_notify (G_OBJECT (self));
360
361 sweeper_game_reset_board (self, width, height);
362 sweeper_game_place_mines (self, n_mines);
363 sweeper_game_count_neighbor_mines (self, width, height);
364
365 g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_items_before, added: width * height);
366
367 g_object_thaw_notify (G_OBJECT (self));
368}
369
370static void
371sweeper_game_init (SweeperGame *self)
372{
373 self->cells = g_ptr_array_new_with_free_func (element_free_func: g_object_unref);
374
375 sweeper_game_new_game (self, width: 8, height: 8, n_mines: 10);
376}
377
378static void
379sweeper_game_end (SweeperGame *self,
380 gboolean win)
381{
382 if (self->playing)
383 {
384 self->playing = FALSE;
385 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_PLAYING]);
386 }
387 if (self->win != win)
388 {
389 self->win = win;
390 g_object_notify_by_pspec (G_OBJECT (self), pspec: game_properties[GAME_PROP_WIN]);
391 }
392}
393
394static void
395sweeper_game_check_finished (SweeperGame *self)
396{
397 guint i;
398
399 if (!self->playing)
400 return;
401
402 for (i = 0; i < self->cells->len; i++)
403 {
404 SweeperCell *cell = g_ptr_array_index (self->cells, i);
405
406 /* There's still a non-revealed cell that isn't a mine */
407 if (!cell->is_visible && !cell->is_mine)
408 return;
409 }
410
411 sweeper_game_end (self, TRUE);
412}
413
414static void
415sweeper_game_reveal_cell (SweeperGame *self,
416 guint position)
417{
418 SweeperCell *cell;
419
420 if (!self->playing)
421 return;
422
423 cell = g_ptr_array_index (self->cells, position);
424 sweeper_cell_reveal (self: cell);
425
426 if (cell->is_mine)
427 sweeper_game_end (self, FALSE);
428
429 sweeper_game_check_finished (self);
430}
431
432G_MODULE_EXPORT void
433minesweeper_cell_clicked_cb (GtkGridView *gridview,
434 guint pos,
435 SweeperGame *game)
436{
437 sweeper_game_reveal_cell (self: game, position: pos);
438}
439
440G_MODULE_EXPORT void
441minesweeper_new_game_cb (GtkButton *button,
442 SweeperGame *game)
443{
444 sweeper_game_new_game (self: game, width: 8, height: 8, n_mines: 10);
445}
446
447static GtkWidget *window = NULL;
448
449GtkWidget *
450do_listview_minesweeper (GtkWidget *do_widget)
451{
452 if (window == NULL)
453 {
454 GtkBuilder *builder;
455
456 g_type_ensure (SWEEPER_TYPE_GAME);
457
458 builder = gtk_builder_new_from_resource (resource_path: "/listview_minesweeper/listview_minesweeper.ui");
459 window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));
460 gtk_window_set_display (GTK_WINDOW (window),
461 display: gtk_widget_get_display (widget: do_widget));
462 g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *) &window);
463
464 g_object_unref (object: builder);
465 }
466
467 if (!gtk_widget_get_visible (widget: window))
468 gtk_widget_show (widget: window);
469 else
470 gtk_window_destroy (GTK_WINDOW (window));
471
472 return window;
473}
474

source code of gtk/demos/gtk-demo/listview_minesweeper.c