四則演算を使ったゲームを作った

# はじめに
お正月に↓の記事を読んだ。

[“どこかで見た広告”風数式パズルゲーム『Formula Warriors』ブラウザ向けに無料公開。二択の数式を選び取り、正しいルートを突き進む](https://automaton-media.com/articles/newsjp/20240104-278055/)

自分で作ってみるのも楽しそう、ということで作ってみた。
最近お気に入りのSiv3Dというフレームワークを使って作った。
楽しかった。

# 作ったもの
<iframe width="315" height="560" src="https://www.youtube.com/embed/H_8WO6_VU0M" title="四則演算でゲーム" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

効果音：Springin’ Sound Stock
音楽：魔王魂
画像：ぴぽや https://pipoya.net/

## 画面の説明
プレイヤーは猫たち。
基本的には近くの敵を追跡して交戦する。マウスでクリックするとそこに集合させることも出来る。
上のバーはHPの合計。
四則演算の結果に従って猫が増減する（□が猫の数）。

# ソースコード
```c++
# include <Siv3D.hpp> // Siv3D v0.6.13
#include <random>
#include <vector>
#include <algorithm>
#include <string>
using namespace s3d;
using namespace std;
const int width = 600;
const int height = 600;
const int h = 100;
const Vec2 npos(-1, -1);

int sign(int i)
{
	if (i > 0) return 1;
	else if (i < 0) return -1;
	else return 0;
}

class Mob
{
private:
	Texture texture;
	int frame;
	int df;
	Font font{ 20 };
	bool player;

	void move(const Vec2& dpos) // dposだけ移動する
	{
		pos += dpos;
	}
	void move_to(const Vec2& pos)
	{
		Vec2 direction(pos - this->pos);
		direction.normalize();
		move(velocity * direction);
	}

	void get_nearest_enemy(const std::vector<Mob>& enemy, int& m_min, double& d_min)
	{
		int M = enemy.size();
		d_min = 1e15;
		for (int m = 0; m < M; m++)
		{
			double d = Geometry2D::Distance(this->pos, enemy[m].pos);
			if (d < d_min && d>0)
			{
				d_min = d;
				m_min = m;
			}
		}
	}
	
public:
	int HP;
	Vec2 pos;
	double velocity;
	int max_damege;
	
	Mob(const char32_t* path,  const Vec2 &pos, int HP=100, double velocity=1, int max_damege=10) : texture(path), frame{ 0 }, df{ 1 }, HP(HP), pos(pos), velocity(velocity), max_damege(max_damege)
	{
	}

	void act(std::vector<Mob>& enemy, Vec2 pos=npos)
	{
		int m_min;
		double d_min;
		get_nearest_enemy(enemy, m_min, d_min);
		if (d_min < 20) // 近接
		{
			int damege = Random(max_damege);
			font(damege).drawAt(enemy[m_min].pos + Vec2(0, -30), ColorF{ 1.0, 0.0, 0.0 });
			enemy[m_min].HP -= damege;
		}

		if (d_min > 10 && pos==npos)
		{
			move_to(enemy[m_min].pos);
		}
		else
		{
			move_to(pos);
		}
	}

	void draw()
	{
		double u = 1. / 3. * frame;
		double v = 0;
		double w = 1. / 3.;
		double h = 1. / 4.;
		texture.uv(u, v, w, h).drawAt(pos);
		frame += df;
		if (frame == 2 || frame == 0) df *= -1;
	}
};

s3d::String opstring(int optype)
{
	s3d::String ret;
	if (optype == 0) ret = U"＋";
	else if (optype == 1) ret = U"－";
	else if (optype == 2) ret = U"×";
	else ret = U"÷";

	return ret;
}

double op(double a, double b, int optype)
{
	double ret;
	if (optype == 0)
	{
		ret = a + b;
	}
	else if (optype == 1)
	{
		ret = a - b;
	}
	else if (optype == 2)
	{
		ret = a * b;
	}
	else
	{
		ret = a / b;
	}
	return ret;
}

void Main()
{
	Window::Resize(width, height + h);
	const Texture background_op{U"img/op.jpg"};
	RectF rect_background_op = background_op.region(Point(0, 0));
	

	while (!rect_background_op.leftClicked())
	{
		background_op.draw(Point(0, 0));
		System::Update();
		System::Sleep(0.01s);
	}

	const Audio audio{ U"bgm/battle.mp3" };
	audio.play();
	const Audio audio_button{ U"bgm/button.mp3" };
	const Texture background{ U"img/pipo-battlebg001.jpg" };
	RectF rect_background = background.region(Point(0, h));

	int M = 15, M_enemy = 80;
	std::vector<Mob> neko;
	std::vector<Mob> enemy;
	for (int m = 0; m < M; m++)
	{
		 neko.push_back(Mob(U"img/neko.png",  RandomVec2(rect_background)));
	}
	for (int m = 0; m < M_enemy; m++)
	{
		enemy.push_back(Mob(U"img/enemy.png", RandomVec2(rect_background)));
	}

	Font font(30, Typeface::Bold);
	int t = 0;
	s3d::String str_op[2];
	bool disp_op = false;
	bool OP = false;
	double num[2];

	Vec2 pos_op[2];
	pos_op[0] = Vec2(10, h / 2);
	pos_op[1] = Vec2(200, h / 2);
	int op_type[2];
	ColorF op_color[2];
	for (int opp = 0; opp < 2; opp++)
	{
		op_color[opp] = ColorF{ 0,0,0 };
	}
	Font sfont(15);
	int Top = 10; // 計算を表示するタイミング
	int DT = 30; // 計算を表示している時間

	while (System::Update())
	{
		background.draw(Vec2(0,h));
		M = neko.size();

		// 演算の表示
		RectF rect_op[2];
		for (int opp = 0; opp < 2; opp++)
		{
			rect_op[opp] = font(str_op[opp]).region(pos_op[opp]);
			rect_op[opp].draw().drawFrame(1, 1, Palette::Orange);; // 演算文字列がないときは表示されない（サイズゼロなので）
		}

		// 演算の実行
		for (int opp = 0; opp < 2; opp++)
		{
			if (disp_op && rect_op[opp].leftClicked() & !OP)
			{
				audio_button.play();
				op_color[opp] = ColorF{ 1,0,0 };
				OP = true;
				M = op(M, num[opp], op_type[opp]);
				if (M <= 1) M = 1;
				int nekosize = neko.size();
				if (M >= neko.size())
				{
					for (int m = 0; m < M - nekosize; m++)
					{
						neko.push_back(Mob(U"img/neko.png", RandomVec2(rect_background)));
					}
				}
				else
				{
					for (int m = 0; m < nekosize - M; m++)
					{
						neko.pop_back();
					}
				}
			}
			font(str_op[opp]).draw(pos_op[opp], op_color[opp]);
		}

		if (t == Top)
		{
			double test = 0;
			while (test < 1) // 2択のどちらかは得をするように
			{
				for (int opp = 0; opp < 2; opp++) // 2択の生成
				{
					num[opp] = Random(0.1, 5.0);
					op_type[opp] = Random(0, 3);
					str_op[opp] = U"□ " + opstring(op_type[opp]) + U" {:.1f}"_fmt(num[opp]);
				}
				test = std::max<double>(op(1, num[0], op_type[0]), op(1, num[1], op_type[1]));
			}
			disp_op = true;
		}
		if (t == Top+DT) // 演算を削除（タイムオーバー）
		{
			for (int opp = 0; opp < 2; opp++)
			{
				op_color[opp] = ColorF{ 0,0,0 };
				str_op[opp] = U"";
			}
			t = 0;
			disp_op = false;
			OP = false;
		}

		int HPsum = 0;
		for (auto& e : neko)
		{
			if (rect_background.leftPressed()) // クリックしているところに向かう
			{
				e.act(enemy, Cursor::PosF());
			}
			else // 近くの敵に向かう
			{
				e.act(enemy);
			}
			e.draw();
			HPsum += (e.HP < 0 ? 0 : e.HP);
		}
		int HPsume = 0;
		for (auto& e : enemy)
		{
			e.act(neko);
			e.draw();
			HPsume += (e.HP<0 ? 0 : e.HP);
		}
		Rect(10, 10, HPsum/10, 10).draw(Palette::Blue);
		Rect(10, 30, HPsume/10, 10).draw(Palette::Red);
		sfont(U"cat: {}"_fmt(neko.size())).draw(Vec2(0, 8));

		// 死亡判定
		neko.erase(std::remove_if(neko.begin(), neko.end(), [&](const Mob& mob) {return (mob.HP < 0); }), neko.end());
		enemy.erase(std::remove_if(enemy.begin(), enemy.end(), [&](const Mob& mob) {return (mob.HP < 0); }), enemy.end());

		if (neko.size() == 0 || enemy.size() == 0)
		{
			System::Sleep(1s);
			break;
		}
		System::Sleep(0.1s);

		t += 1;
	}

	const Texture background_hc(U"img/highsccore.jpg");
	font = Font(40);
	while(System::Update())
	{
		background_hc.draw();
		
		font(U"猫の数：{}"_fmt(neko.size())).draw(Point(10, height / 2));
		if (MouseL.pressed()) break;
		System::Sleep(0.1s);
	}
}

```