はじめに
ふと思い立ったのでターミナルでプレイするマインスイーパつくってみました.
マインスイーパとは?
マインスイーパ(Minesweeper)は1980年代に発明された,一人用のコンピュータゲームである.ゲームの目的は地雷原から地雷を取り除くことである.
このゲームは多数のコンピュータプラットフォーム向けに書き直されており,Microsoft Windowsをはじめ,LinuxのGNOMEやKDEなどのシステムに同梱されているものもある.
wikipediaより引用:https://ja.wikipedia.org/wiki/マインスイーパ
コード
以下のように実装しました.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
class Mine {
public:
Mine(int h, int w, int b);
~Mine();
void start();
void print_matrix();
void print_correct();
void get_point(char msg[], int *pt, int threshold);
bool open(int y, int x);
bool is_clear();
private:
int **matrix;
bool **map;
int height;
int width;
int bombs;
int size;
void create_check_array(bool *check);
void create_matrix_and_map(bool *check);
bool is_number(char str[]);
};
#include "minesweeper.hpp"
Mine::Mine(int h, int w, int b) {
height = h;
width = w;
bombs = b;
size = height * width;
matrix = (int **)malloc(sizeof(int *) * height);
map = (bool **)malloc(sizeof(bool *) * height);
srand((unsigned int)time(NULL));
bool *check = (bool *)malloc(sizeof(bool) * size);
create_check_array(check);
create_matrix_and_map(check);
free(check);
}
Mine::~Mine() {
free(matrix);
free(map);
}
void Mine::start() {
printf("\n\x1b[37m !!!size:%dx%d, bombs:%d start!!!\n\x1b[39m", height, width, bombs);
}
void Mine::print_matrix() {
printf(" ");
for(int x = 0; x < width; x++) {
printf("%3d", x);
}
printf("\n");
printf(" ");
for(int x = 0; x < width; x++) {
printf("---");
}
printf("\n");
for(int y = 0; y < height; y++) {
printf("%3d |", y);
for(int x = 0; x < width; x++) {
if(map[y][x] == false) {
printf(" ?");
}
else {
int val = matrix[y][x];
if(val > 0) {
printf("\x1b[37m%3d\x1b[39m", val);
}
else if(val == 0) {
printf(" -");
}
else if(val == -1) {
printf("\x1b[31m *\x1b[39m");
}
}
}
printf("\n");
}
}
void Mine::print_correct() {
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
map[y][x] = true;
}
}
print_matrix();
}
void Mine::get_point(char msg[], int *pt, int threshold) {
while(1) {
char str[8] = {};
printf("%s(0 ~ %d) ", msg, threshold - 1);
scanf("%s", str);
if(is_number(str) && sscanf(str, "%d", pt) == 1) {
if(*pt >= 0 && *pt < threshold) {
break;
}
}
}
}
bool Mine::open(int y, int x) {
int val = matrix[y][x];
if(val == -1) {
return false;
}
else if(val == 0) {
for(int j = -1; j <= 1; j++) {
for(int i = -1; i <= 1; i++) {
int _y = y + j, _x = x + i;
if(_y < 0 || _x < 0 || _y >= height || _x >= width) {
continue;
}
else if(!map[_y][_x]) {
map[_y][_x] = true;
open(_y, _x);
}
}
}
return true;
}
else {
map[y][x] = true;
return true;
}
}
bool Mine::is_clear() {
int count = 0;
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
if(!map[y][x]) {
count++;
}
}
}
if(count == bombs) {
return true;
}
else {
return false;
}
}
void Mine::create_check_array(bool *check) {
int _bombs = 2 * bombs < size ? bombs : size - bombs;
int *random = (int *)malloc(sizeof(int) * _bombs);
for(int _b = 0; _b < _bombs; _b++) {
while(1) {
int val = rand() % size;
if(!check[val]) {
check[val] = true;
random[_b] = val;
break;
}
}
}
if(_bombs != bombs) {
for(int i = 0; i < size; i++) {
check[i] = !check[i];
}
}
free(random);
}
void Mine::create_matrix_and_map(bool *check) {
for(int y = 0; y < height; y++) {
matrix[y] = (int *)malloc(sizeof(int) * width);
map[y] = (bool *)malloc(sizeof(bool) * width);
for(int x = 0; x < width; x++) {
int index = y * width + x;
if(check[index]) {
matrix[y][x] = -1;
}
map[y][x] = false;
}
}
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
int val = matrix[y][x];
if(val != -1) {
int count = 0;
for(int j = -1; j <= 1; j++) {
for(int i = -1; i <= 1; i++) {
int _y = y + j, _x = x + i;
if(_y < 0 || _x < 0 || _y >= height || _x >= width) {
continue;
}
else if(matrix[_y][_x] == -1) {
count++;
}
}
}
matrix[y][x] = count;
}
}
}
}
bool Mine::is_number(char str[]) {
for(int i = 0; str[i]; i++) {
if(str[i] < '0' || str[i] > '9') {
return false;
}
}
return true;
}
#include "minesweeper.hpp"
int main(int argc, char* argv[]) {
int height = argc == 4 ? atoi(argv[1]) : 10;
int width = argc == 4 ? atoi(argv[2]) : 10;
int bombs = argc == 4 ? atoi(argv[3]) : 10;
if(bombs > height * width) {
printf("!!!too many bombs!!!\n");
return 0;
}
Mine mine(height, width, bombs);
mine.start();
int pt_y = 0, pt_x = 0;
while(1) {
printf("\n");
mine.print_matrix();
mine.get_point((char *)" input point y: ", &pt_y, height);
mine.get_point((char *)" input point x: ", &pt_x, width);
if(!mine.open(pt_y, pt_x)) {
printf("\x1b[31m\n !!!failed!!!\n\x1b[39m");
mine.print_correct();
break;
}
if(mine.is_clear()) {
printf("\x1b[33m\n !!!success!!!\n\x1b[39m");
mine.print_correct();
break;
}
}
return 0;
}
動作
こんな感じで動きます.引数に高さ,幅,爆弾の総数を渡すことができます.何も渡さなかったらデフォルトのサイズ10×10,爆弾数10でゲームがスタートします.
サイズ20×20,爆弾数400でやるとこんな感じ.
ふざけました.
ポイント
ポイントは以下の3つです.
・mallocを使った2次元配列の生成
・openメソッドを再帰
・重複のないランダム配列の生成
mallocを使った2次元配列の生成
下のようにmallocを使うことで,2次元配列を動的に生成することができます.
vectorのvectorでも可能ですが,今回はmalloc使ってみました.
int **matrix = (int **)malloc(sizeof(int *) * height);
for(int y = 0; y < height; y++) {
matrix[y] = (int *)malloc(sizeof(int) * width);
}
こんなふうに書くと,階段みたいな2次元配列(のようなもの?)もつくれます.
int N = 10;
int **matrix = (int **)malloc(sizeof(int *) * N);
for(int j = 0; j < N; j++) {
matrix[j] = (int *)malloc(sizeof(int) * (j + 1));
for(int i = 0; i <= j; j++) {
matrix[j][i] = j * i;
}
}
/*
0
0 1
0 2 4
0 3 6 9
0 4 8 12 16
0 5 10 15 20 25
0 6 12 18 24 30 36
0 7 14 21 28 35 42 49
0 8 16 24 32 40 48 56 64
0 9 18 27 36 45 54 63 72 81
*/
openメソッドの再帰
「周囲の爆弾の数が0個のマス」を開けたとき周囲の8個のマスを同時に開きます.
このとき周囲の8個のマスの中に同じように「周囲の爆弾の数が0個のマス」があった場合,そのマスの周囲8個のマスも開きます.
どんな動きか想像しづらいと思うので,先に画像をお見せします.
下の画像では(0, 0)のマスしか開いていないのに,一気にマスが開いています.これは,(0, 0)のマスの周囲には爆弾が存在しないので,その周囲の(0, 1), (1, 0), (1, 1)も開きます.
すると,(1, 0)のマスの周囲にも爆弾が存在しないので,周辺のまだ開いていない(2, 0), (2, 1)を開きます.これが連続しておこるため,一気にマスが開きます.
ではコードです.
開いたマスの周辺の爆弾の数が0なので,else if(val == 0)に入ります.周囲のマスの中でまだ開いていないマスの座標を引数にopenを呼んでいます.
これにより一気に開く実装ができます.
bool Mine::open(int y, int x) {
int val = matrix[y][x];
if(val == -1) {
return false;
}
else if(val == 0) {
for(int j = -1; j <= 1; j++) {
for(int i = -1; i <= 1; i++) {
int _y = y + j, _x = x + i;
if(_y < 0 || _x < 0 || _y >= height || _x >= width) {
continue;
}
else if(!map[_y][_x]) {
map[_y][_x] = true;
open(_y, _x); // ここがポイント
}
}
}
return true;
}
else {
map[y][x] = true;
return true;
}
}
重複のないランダム配列の生成
重複のないランダムな値が格納された配列を生成するには以下のようにしています.
ここはあまり自信がないので,もっといい方法あるよって方は教えて下さい.
爆弾を置く座標を乱数で決定し,これがまだ登録されていない値だったら採用,すでに登録済みであったら不採用にしています.
サイズの50%以上を爆弾が占める場合は,爆弾を置かない座標を乱数で決定し,最後に反転させれば小さなサイズの配列で爆弾を配置できます.
void Mine::create_check_array(bool *check) {
int _bombs = 2 * bombs < size ? bombs : size - bombs;
int *random = (int *)malloc(sizeof(int) * _bombs);
for(int _b = 0; _b < _bombs; _b++) {
while(1) {
int val = rand() % size;
if(!check[val]) {
check[val] = true;
random[_b] = val;
break;
}
}
}
// サイズの50%以上を爆弾が占める場合の反転作業
if(_bombs != bombs) {
for(int i = 0; i < size; i++) {
check[i] = !check[i];
}
}
free(random);
}
最後に
自分で実装したからか実際にプレイしてみると案外楽しいです.座標を毎回入力するのがかなり面倒ですが...
もっとこう書いたらいいよ!そこおかしくない?みたいなのあったら教えて下さい.