-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathMain.cpp
418 lines (349 loc) · 10.3 KB
/
Main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# include <Siv3D.hpp> // OpenSiv3D v0.6.5
// ゲームの状態
enum class GameState
{
Game, // ゲームが進行中
Failed, // ゲームオーバー
Cleared, // ゲームクリア
};
// セルの状態
struct CellState
{
// 開かれている
bool opened = false;
// 旗が立てられている
bool flagged = false;
// 爆発した
bool exploded = false;
// 島番号(数字のないマスを一気に開くときに使う)
int32 groupIndex = 0;
};
// 周囲のマスへのオフセット
constexpr Point Offsets[8] =
{
{ -1, -1 }, { 0, -1 }, { 1, -1 },
{ -1, 0 } , { 1, 0 },
{ -1, 1 }, { 0, 1 }, { 1, 1 },
};
// 指定したマス目の周囲にある 💣 (-1) の個数を返す関数
int32 GetBombCount(const Grid<int32>& grid, const Point& center)
{
// 自身が 💣 (-1) なら -1 を返す
if (grid[center] == -1)
{
return -1;
}
// 見つかった 💣 (-1) の個数
int32 bombCount = 0;
for (const auto& offset : Offsets)
{
// 調べるマス
const Point pos = (center + offset);
// grid.fetch(pos, defaultValue) は、
// pos が範囲内の場合 grid[pos] を返し、それ以外の場合は defaultValue を返す
if (grid.fetch(pos, 0) == -1) // 💣 (-1) の場合
{
++bombCount;
}
}
return bombCount;
}
// 盤面を生成する関数
Grid<int32> MakeGame(const Size& size, int32 bombs)
{
// 盤面の二次元配列を作成する
Grid<int32> grid(size);
// 指定された個数だけ 💣 (-1) を設置する
while (bombs)
{
// 二次元配列上のランダムな位置
const Point pos = RandomPoint((size.x - 1), (size.y - 1));
// 未設置であれば
if (grid[pos] == 0)
{
// 💣 (-1) を設置する
grid[pos] = -1;
// 残りの 💣 の個数を減らす
--bombs;
}
}
// すべてのマスについて
for (int32 y = 0; y < size.y; ++y)
{
for (int32 x = 0; x < size.x; ++x)
{
// 数字を計算する。ただし、💣 マスは -1 のまま
grid[y][x] = GetBombCount(grid, Point{ x, y });
}
}
return grid;
}
// 盤面の状態を作成する関数
Grid<CellState> MakeStates(const Grid<int32>& grid)
{
const Size size = grid.size();
// 盤面と同じ大きさの二次元配列
Grid<CellState> states(size);
// 各マスの接続状況を管理するデータ構造
DisjointSet<int32> ds{ states.num_elements() };
// すべてのマスについて
for (int32 y = 0; y < size.y; ++y)
{
for (int32 x = 0; x < size.x; ++x)
{
// 自身のマスのインデックス
const int32 index = static_cast<int32>(y * size.x + x);
// 自身が 0 のマスで
if (grid[y][x] == 0)
{
// 右のマスが 0 なら
if (int nx = (x + 1);
(nx < size.x) && (grid[y][nx] == 0))
{
const int32 east = (index + 1); // 右のマスのインデックス
ds.merge(index, east); // 右のマスを同じ島にする
}
// 下のマスが 0 なら
if (int ny = (y + 1);
(ny < size.y) && (grid[ny][x] == 0))
{
const int32 south = (index + size.x); // 下のマスのインデックス
ds.merge(index, south); // 下のマスを同じ島にする
}
}
}
}
{
// マスのインデックス
int32 index = 0;
// すべてのマスについて
for (int32 y = 0; y < size.y; ++y)
{
for (int32 x = 0; x < size.x; ++x)
{
// 島番号を割り当て
states[y][x].groupIndex = ds.find(index);
++index;
}
}
}
return states;
}
// 開いていないセルのブロックを描く関数
void DrawBlock(const Rect& rect)
{
Triangle{ rect.tl(), rect.tr(), rect.bl() }.draw(ColorF{ 1.0 });
Triangle{ rect.tr(), rect.br(), rect.bl() }.draw(ColorF{ 0.5 });
rect.stretched(-5).draw(ColorF{ 0.75 });
}
// 盤面を描画する関数
void DrawGame(const Grid<int32>& grid, const Grid<CellState>& states, const Font& font, const Texture& bombTexture, const Texture& flagTexture, const Point& gamePos, const Size& cellSize)
{
// 0~8 の数字の色
constexpr ColorF NumberColors[9] =
{
ColorF{ 0, 0, 0 }, ColorF{ 0, 0, 1 }, ColorF{ 0, 0.5, 0 }, ColorF{ 1, 0, 0 },
ColorF{ 0, 0, 0.5 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }, ColorF{ 0.5, 0, 0 }
};
// すべてのマスについて
for (int32 y = 0; y < grid.height(); ++y)
{
for (int32 x = 0; x < grid.width(); ++x)
{
const auto& state = states[y][x];
// セルの左上座標
const Point pos = (gamePos + (cellSize * Point{ x, y }));
// セルの領域
const Rect cell{ pos, cellSize };
if (state.opened) // 開かれている
{
// 背景を描く
cell.stretched(-1).draw(ColorF{ 0.75 });
if (const int32 n = grid[y][x];
n == -1) // 💣 (-1) マスであれば
{
// 爆発箇所であればセルを赤に
if (state.exploded)
{
cell.stretched(-1).draw(ColorF{ 1, 0, 0 });
}
// 爆弾を描く
bombTexture.resized(36).drawAt(cell.center());
}
else if (1 <= n) // 1 以上の数字マスであれば
{
// 数字を描く
font(n).drawAt(cell.center(), NumberColors[n]);
}
}
else // 開かれていない
{
// ブロックを描く
DrawBlock(cell);
// 旗が立てられているなら旗を描く
if (state.flagged)
{
flagTexture.resized(30).drawAt(cell.center());
}
}
}
}
}
// 指定された島番号のマスと、それらに隣接するマスを開く関数
void OpenGroup(const Grid<int32>& grid, Grid<CellState>& states, const int32 groupIndex)
{
// すべてのマスについて
for (int32 y = 0; y < grid.height(); ++y)
{
for (int32 x = 0; x < grid.width(); ++x)
{
auto& state = states[y][x];
// 指定された島番号のマスであれば
if (state.groupIndex == groupIndex)
{
// 開く
state.opened = true;
// その周囲のマスについて
for (const auto& offset : Offsets)
{
const Point neighbor = (Point{ x, y } + offset);
// 盤面の範囲内かつ未開放であれば, そのマスも開く
if (grid.inBounds(neighbor) && (not states[neighbor].opened))
{
states[neighbor].opened = true;
// それが異なる島番号の数字のないマス (0) であれば, 再帰的にそれらも開く
if ((grid[neighbor] == 0) && (groupIndex != states[neighbor].groupIndex))
{
OpenGroup(grid, states, states[neighbor].groupIndex);
}
}
}
}
}
}
}
// 盤面を更新する関数
void UpdateGame(GameState& gameState, const Grid<int32>& grid, Grid<CellState>& states, const int32 bombCount, const Point& gamePos, const Size& cellSize)
{
// 盤面の領域
const Rect gameArea{ gamePos, (grid.size() * cellSize - Point{ 1, 1 }) };
// 盤面が左クリックされた
const bool open = gameArea.leftClicked();
// 盤面が右クリックされた
const bool flag = gameArea.rightClicked();
if (open || flag)
{
// クリックされたマスの位置
const Point pos = ((Cursor::Pos() - gamePos) / cellSize);
if (open && (not states[pos].opened) && (not states[pos].flagged)) // 開かれていない、旗のないマスが左クリックされた
{
// そのマスを開く
states[pos].opened = true;
// そのマスが数字のないマス (0) であれば
if (grid[pos] == 0)
{
// 同じ島番号のマスと、それらに隣接するマスも開く
OpenGroup(grid, states, states[pos].groupIndex);
}
// そのマスが 💣 (-1) であれば
if (grid[pos] == -1)
{
// ゲームオーバーにする
gameState = GameState::Failed;
// 爆発したフラグを立てる
states[pos].exploded = true;
// すべての 💣 マスを開く
for (int32 y = 0; y < grid.height(); ++y)
{
for (int32 x = 0; x < grid.width(); ++x)
{
if (grid[y][x] == -1)
{
states[y][x].opened = true;
}
}
}
}
else if (states.count_if([](const CellState& c) { return (not c.opened); }) == bombCount)
{ // 開かれていないマスの個数が爆弾の個数と一致すれば
// ゲームクリアにする
gameState = GameState::Cleared;
}
}
else if (flag) // 右クリックされた
{
// 旗の状態を反転
states[pos].flagged = (not states[pos].flagged);
}
}
}
void Main()
{
// 背景色をやや暗い灰色にする
Scene::SetBackground(ColorF{ 0.5 });
// 盤面のマス目の数
constexpr Size GameSize{ 20, 13 };
// 設置する 💣 の個数
constexpr int32 BombCount = 30;
// 💣 の個数がマス目の 4 分の 1 以上の場合はコンパイルエラーにする
static_assert(BombCount < (GameSize.area() / 4));
// セルの大きさ
constexpr Size CellSize{ 40, 40 };
// 盤面の描画位置
constexpr Size GamePos{ 0, 80 };
// 数字用のフォント
const Font font{ FontMethod::MSDF, 32, Typeface::Bold };
// 爆弾の絵文字
const Texture bombTexture{ U"💣"_emoji };
// 旗の絵文字
const Texture flagTexture{ U"🚩"_emoji };
// GameState に対応する顔絵文字
const std::array<Texture, 3> faceTextures = { Texture{ U"🙂"_emoji }, Texture{ U"😵"_emoji }, Texture{ U"😎"_emoji } };
// 盤面を作成する
Grid<int32> grid = MakeGame(GameSize, BombCount);
// 各セルの状態を作成する
Grid<CellState> states = MakeStates(grid);
// ゲームの状態
GameState gameState = GameState::Game;
// 顔ボタンの領域
const Rect faceButton{ Arg::center(Scene::Width() / 2, 40), 72 };
while (System::Update())
{
////////////////////////////////
//
// 状態の更新
//
////////////////////////////////
{
// ゲームが進行中なら盤面を更新
if (gameState == GameState::Game)
{
UpdateGame(gameState, grid, states, BombCount, GamePos, CellSize);
}
// 顔ボタンが押されたら状態を初期化
if (faceButton.leftClicked())
{
grid = MakeGame(GameSize, BombCount);
states = MakeStates(grid);
gameState = GameState::Game;
}
}
////////////////////////////////
//
// 描画
//
////////////////////////////////
{
// 盤面を描く
DrawGame(grid, states, font, bombTexture, flagTexture, GamePos, CellSize);
// UI エリアの背景を描く
Rect{ Scene::Width(), 80 }.draw(ColorF{ 0.75 });
{
// 顔ボタンを描く
DrawBlock(faceButton);
// 顔を描く
faceTextures[FromEnum(gameState)].resized(60).drawAt(faceButton.center());
}
}
}
}