お手頃なゲームを作りたいと思いお題を探したらふとマインスイーパーを思い出したので、C#で作ってみました。
昔はwindowsにも標準搭載されてたあれです。
まずはルール確認(wikipediaより引用
マインスイーパ - Wikipedia
)
ゲーム画面は正方形のマスが敷き詰められた長方形のフィールドから構成されている。それぞれのマスは開けることができるが、地雷の置かれているマスを開けると負けとなる。地雷の置かれていないマスを開けたときは、隣接する8方向のマスのいずれかに地雷がある場合はその個数が表示され、隣接するマスに地雷が置かれていないときは、それらが自動的に開けられる。地雷の置かれていないマスをすべて開ければ勝ちとなる。
ルールは仕様は下記とします。
- 今回は、爆弾のマスは9x9の爆弾の数は10個とする。
- 各マスは、「爆弾」または「隣接する爆弾の数」で構成される。
- 爆弾のマスを開いた場合はゲーム終了(負け)とする。
- すべての爆弾が置かれていないマスを開いた場合はゲーム終了(勝ち)とする。
- 開いてないマスが全て爆弾マスの場合でもゲーム終了とする。
- 開いたマスが爆弾のないマスの場合は、隣接する爆弾の数を表示する。
- →隣接する爆弾がない場合は、隣接する爆弾が存在するマスまで自動的に表示する。
- 開いたマスの数字と周囲の旗の数が一致している状態で、数字マスをクリックした場合は、周囲のマスを全て空ける。(便利機能)
開発言語
- .net 8.0 C#
- タイマー等のデジタル表示には、「SevenSegment 1.0.0」を利用してます。
とりあえず、完成イメージ
ダウンロード
ソース全体は、
github.com
まずはブロック(マス)を作成
ラベルを継承して、作成してます。
ブロックの状態を管理して、状態が変更された際に適切な描画をするようにしています。
画像等は爆弾と旗はここから貰った画像を加工して使ってそれ以外は文字で表示してます。
using minesweeper.Properties;
namespace minesweeper
{
enum BlockState
{
Opened,
Closed,
Flagged,
Flagged_NG,
Bomb,
Bomb_OPENDED,
}
internal class Positon(int x, int y)
{
public int X { get; set; } = x;
public int Y { get; set; } = y;
}
internal class Block : Label
{
public Block()
{
TextAlign = ContentAlignment.MiddleCenter;
BackgroundImageLayout = ImageLayout.Stretch;
Font = new Font("Arial", 15, FontStyle.Bold);
AutoSize = false;
SetImage();
}
private bool _IsPressed = false;
public bool _IsOpened = false;
private BlockState state = BlockState.Closed;
public BlockState State
{
get { return state; }
set
{
state = value;
SetImage();
}
}
public int BombCount = 0;
public bool IsBomb = false;
public Positon Positon = new Positon(0, 0);
public bool IsPressed
{
get { return _IsPressed; }
set
{
_IsPressed = value;
SetImage();
}
}
private void SetImage()
{
if (_IsPressed)
{
BackgroundImage = Resources.block_pressed;
return;
}
switch (state)
{
case BlockState.Opened:
BackgroundImage = Resources.block_pressed;
SetBombCountText();
break;
case BlockState.Closed:
BackgroundImage = Resources.block;
break;
case BlockState.Flagged:
BackgroundImage = Resources.flag;
break;
case BlockState.Flagged_NG:
BackgroundImage = Resources.flag;
Text = "X";
Font = new Font("Arial", 18, FontStyle.Bold);
ForeColor = Color.Red;
break;
case BlockState.Bomb:
BackgroundImage = Resources.bomb;
BackColor = ColorTranslator.FromHtml("#c6c6c6");
break;
case BlockState.Bomb_OPENDED:
BackgroundImage = Resources.bomb;
BackColor = Color.Red;
break;
}
}
private void SetBombCountText()
{
if (BombCount == 0) return;
Text = BombCount.ToString();
switch (BombCount)
{
case 1:
ForeColor = ColorTranslator.FromHtml("#0000F7");
break;
case 2:
ForeColor = ColorTranslator.FromHtml("#007C00");
break;
case 3:
ForeColor = ColorTranslator.FromHtml("#EC1F1F");
break;
case 4:
ForeColor = ColorTranslator.FromHtml("#00007C");
break;
case 5:
ForeColor = ColorTranslator.FromHtml("#7C0000");
break;
case 6:
ForeColor = ColorTranslator.FromHtml("#007C7C");
break;
case 7:
ForeColor = ColorTranslator.FromHtml("#000000");
break;
case 8:
ForeColor = ColorTranslator.FromHtml("#7C7C7C");
break;
}
}
}
}
次はゲーム画面の作成
InitGameFiled()で盤面を作成しています。
動的に上記で作成したBlockを並べてランダムで爆弾の位置を設定しています。
細かい動作はソースコメントで。
ドラッグ中はドラッグを開始したコントロールが以外イベントが発生しないようなので、
マウス座標から現在のコントロール(マス)を特定するようにしてます。
using minesweeper.Properties;
using Timer = System.Windows.Forms.Timer;
namespace minesweeper
{
public partial class Form1 : Form
{
enum GameStatus
{
Ready,
Playing,
GameEnd,
}
const int xLength = 9;
const int yLength = 9;
const int bombCount = 10;
const int BlockSixe = 50;
Timer timer = new Timer();
GameStatus gameState = GameStatus.Ready;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
timer.Interval = 1000;
txtBombCount.Value = "";
timer.Tick += Timer_tick;
InitGameFiled();
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
void Timer_tick(object? sender, EventArgs e)
{
this.txtBombTimer.Value = (int.Parse(this.txtBombTimer.Value) + 1).ToString();
}
<summary>
</summary>
private void InitGameFiled()
{
this.txtBombCount.Value = bombCount.ToString();
this.txtBombTimer.Value = "0";
timer.Stop();
this.gameState = GameStatus.Ready;
blockArea.Controls.Clear();
for (int x = 0; x < xLength; x++)
{
for (int y = 0; y < yLength; y++)
{
var label = new Block();
label.Name = "Block" + x + y;
label.Size = new Size(BlockSixe, BlockSixe);
label.Location = new Point(x * BlockSixe, y * BlockSixe);
label.Positon = new Positon(x, y);
label.MouseUp += Block_MouseUp;
label.MouseDown += (sender, e) =>
{
if (this.gameState == GameStatus.GameEnd) return;
if ((e.Button & MouseButtons.Left) != MouseButtons.Left) return;
if (this.gameState == GameStatus.Ready)
{
this.gameState = GameStatus.Playing;
timer.Start();
}
PressBlock(label);
};
label.MouseMove += Block_MouseMove;
blockArea.Controls.Add(label);
}
}
Random rnd = new Random();
var setBombCount = 0;
while (setBombCount != bombCount)
{
int x = rnd.Next(0, xLength);
int y = rnd.Next(0, yLength);
var block = (Block?)blockArea.Controls["Block" + x + y];
if (block.IsBomb)
{
continue;
}
setBombCount++;
block.IsBomb = true;
}
foreach (Control control in blockArea.Controls)
{
if (control == null && (control is not Block)) return;
Block block = (Block)control;
foreach (var target in GetAroundBlok(block))
{
if (target.IsBomb) block.BombCount++;
}
}
this.Size = new Size(BlockSixe * xLength + 20, BlockSixe * yLength + infoArea.Height + SystemInformation.CaptionHeight + 20);
this.FormBorderStyle = FormBorderStyle.FixedSingle;
}
<summary>
</summary>
<param name="block"></param>
<returns></returns>
private List<Block> GetAroundBlok(Block block)
{
List<Block> blocks = new List<Block>();
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
if (block.Positon.X + x < 0 || block.Positon.X + x >= xLength || block.Positon.Y + y < 0 || block.Positon.Y + y >= yLength) continue;
var target = (Block?)blockArea.Controls["Block" + (block.Positon.X + x) + (block.Positon.Y + y)];
if (target == null) return blocks;
blocks.Add(target);
}
}
return blocks;
}
<summary>
</summary>
<returns></returns>
private Block? GetActiveBlock()
{
Control? control = blockArea.GetChildAtPoint(blockArea.PointToClient(Cursor.Position));
if (control == null && (control is not Block)) return null;
return (Block)control;
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
private void Block_MouseUp(object? sender, MouseEventArgs e)
{
if (this.gameState == GameStatus.GameEnd) return;
ResetPreessedBlock(null);
var block = this.GetActiveBlock();
if (block == null) return;
if ((e.Button & MouseButtons.Left) == MouseButtons.Left && block.State != BlockState.Flagged)
{
if (block.IsBomb)
{
GameOver(block);
return;
}
if (block.State == BlockState.Opened && CanBlockOpen(block))
{
OpenBlock(block, true);
CheckGameClear();
return;
}
if (block.BombCount == 0)
{
OpenBlock(block, false);
}
else
{
block.State = BlockState.Opened;
}
}
if ((e.Button & MouseButtons.Right) == MouseButtons.Right && block.State != BlockState.Opened)
{
if (block.State == BlockState.Flagged)
{
block.State = BlockState.Closed;
}
else
{
block.State = BlockState.Flagged;
}
}
CheckGameClear();
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
private void Block_MouseMove(object? sender, MouseEventArgs e)
{
if (this.gameState == GameStatus.GameEnd) return;
if ((Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) return;
var block = GetActiveBlock();
if (block == null) return;
if (block.State == BlockState.Opened) return;
block.IsPressed = true;
ResetPreessedBlock(block);
}
<summary>
</summary>
<param name="current"></param>
private void ResetPreessedBlock(Block? current)
{
foreach (Control control in blockArea.Controls)
{
if (control == null && (control is not Block)) return;
Block block = (Block)control;
if (block.State == BlockState.Closed && current != control)
{
((Block)control).IsPressed = false;
}
}
}
<summary>
</summary>
<param name="block"></param>
private void PressBlock(Block block)
{
if (block == null) return;
if (block.State == BlockState.Closed)
{
block.IsPressed = true;
}
else
{
foreach (var target in GetAroundBlok(block))
{
if (target.State == BlockState.Closed)
{
target.IsPressed = true;
}
}
}
}
<summary>
</summary>
<param name="block"></param>
<param name="isOpenBomb"></param>
private void OpenBlock(Block block, bool isOpenBomb)
{
if (block == null) return;
foreach (var target in GetAroundBlok(block))
{
if (target.State != BlockState.Closed) continue;
if (isOpenBomb)
{
if (target.IsBomb)
{
GameOver(target);
return;
}
}
else
{
if (target.IsBomb) continue;
}
target.State = BlockState.Opened;
if (target.BombCount == 0)
{
OpenBlock(target, isOpenBomb);
}
}
}
<summary>
</summary>
<param name="block"></param>
<returns></returns>
private Boolean CanBlockOpen(Block block)
{
if (block == null) return false;
int flagCount = 0;
foreach (var target in GetAroundBlok(block))
{
if (target.State == BlockState.Flagged)
{
flagCount++;
}
}
return block.BombCount == flagCount;
}
<summary>
</summary>
private void CheckGameClear()
{
int flagCount = 0;
int openCount = 0;
int unmacthCount = 0;
foreach (Control control in blockArea.Controls)
{
if (control == null && (control is not Block)) return;
Block block = (Block)control;
if (block.State == BlockState.Flagged)
{
flagCount++;
}
if (block.State == BlockState.Opened)
{
openCount++;
}
if (block.State == BlockState.Flagged && !block.IsBomb)
{
unmacthCount++;
}
}
if (unmacthCount == 0 && openCount == xLength * yLength - bombCount)
{
timer.Stop();
this.btnGame.BackgroundImage = Resources.button_clear;
this.gameState = GameStatus.GameEnd;
foreach (Control control in blockArea.Controls)
{
if (control == null && (control is not Block)) return;
Block block = (Block)control;
if (block.IsBomb)
{
block.State = BlockState.Flagged;
}
}
this.txtBombCount.Value = "0";
}
else
{
this.txtBombCount.Value = (bombCount - flagCount).ToString();
}
}
<summary>
</summary>
<param name="currentBlock"></param>
private void GameOver(Block currentBlock)
{
timer.Stop();
foreach (Control control in blockArea.Controls)
{
if (control == null && (control is not Block)) return;
Block block = (Block)control;
if (block == currentBlock)
{
block.State = BlockState.Bomb_Opened;
continue;
}
if (block.State == BlockState.Flagged && !block.IsBomb)
{
block.State = BlockState.Flagged_NG;
continue;
}
if (block.State != BlockState.Flagged && block.IsBomb)
{
block.State = BlockState.Bomb;
continue;
}
if (block.State == BlockState.Closed)
{
block.State = BlockState.Opened;
continue;
}
}
this.btnGame.BackgroundImage = Resources.button_gameorver;
this.gameState = GameStatus.GameEnd;
}
<summary>
</summary>
<param name="sender"></param>
<param name="e"></param>
private void btnGame_Click(object sender, EventArgs e)
{
InitGameFiled();
this.btnGame.BackgroundImage = Resources.button_default;
}
}
}