OpenSiv3D | ボードゲームのテンプレート集

公開:2020/09/30
更新:2020/10/01
8 min読了の目安(約7600字TECH技術記事

1. リバーシの盤面

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

namespace State
{
	constexpr int32 Void = -1; // 石を置けないマス(特殊ルール)
	constexpr int32 Empty = 0; // 石を置けるマス
	constexpr int32 Black = 1; // 黒が置かれているマス
	constexpr int32 White = 2; // 白が置かれているマス
}

const Array<Grid<int32>> BoardPatterns =
{
	{ // Board A
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 1, 2, 0, 0, 0 },
		{ 0, 0, 0, 2, 1, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
	},
	{ // Board B
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 1, 1, 0, 0, 0 },
		{ 0, 0, 0, 2, 2, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
	},
	{ // Board C
		{ 0, 0, 0, -1, -1, 0, 0, 0 },
		{ 0, 0, 0, -1, -1, 0, 0, 0 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ -1, -1, 0, 1, 2, 0, -1, -1 },
		{ -1, -1, 0, 2, 1, 0, -1, -1 },
		{ 0, 0, 0, 0, 0, 0, 0, 0 },
		{ 0, 0, 0, -1, -1, 0, 0, 0 },
		{ 0, 0, 0, -1, -1, 0, 0, 0 },
	},
};

void DrawBoard(const Grid<int32>& board, const Vec2& boardPos, double cellSize)
{
	for (auto p : step(board.size()))
	{
		const int32 state = board[p];

		if (state == State::Void)
		{
			continue;
		}

		const RectF cell(boardPos + (p * cellSize), cellSize);

		cell.draw(ColorF(0.2, 0.7, 0.3))
			.drawFrame(1, 0, ColorF(0.25));

		if (state == State::Empty)
		{
			continue;
		}

		const ColorF colorTop = (state == State::Black) ? ColorF(0.1) : ColorF(0.9);
		const ColorF colorBottom = (state == State::Black) ? ColorF(0.9) : ColorF(0.1);

		Circle(cell.center(), cellSize * 0.45)
			.drawShadow(Vec2(1, 3), 4, 0)
			.draw(colorBottom)
			.movedBy(0, -2)
			.draw(colorTop);
	}
}

Optional<Point> GetCellIndex(const Vec2& cursorPos, const Grid<int32>& board, const Vec2& boardPos, double cellSize)
{
	const int32 xIndex = static_cast<int32>((cursorPos.x - boardPos.x) / cellSize);
	const int32 yIndex = static_cast<int32>((cursorPos.y - boardPos.y) / cellSize);

	if (!InRange(xIndex, 0, (board.size().x - 1))
		|| !InRange(yIndex, 0, (board.size().y - 1)))
	{
		return none;
	}

	if (board[yIndex][xIndex] == State::Void)
	{
		return none;
	}

	return Point(xIndex, yIndex);
}

void Main()
{
	Scene::SetBackground(ColorF(0.8, 0.7, 0.6));

	size_t boardPatternIndex = 0;
	Grid<int32> board = BoardPatterns[boardPatternIndex];
	constexpr Vec2 boardPos(40, 40);
	constexpr double cellSize = 64;

	while (System::Update())
	{
		DrawBoard(board, boardPos, cellSize);

		if (SimpleGUI::RadioButtons(boardPatternIndex,
			{ U"Board A", U"Board B", U"Board C" }, Vec2(600, 40)))
		{
			board = BoardPatterns[boardPatternIndex];
		}

		if (const Optional<Point> cellIndex = GetCellIndex(Cursor::Pos(), board, boardPos, cellSize))
		{
			RectF(boardPos + (*cellIndex * cellSize), cellSize)
				.draw(ColorF(1.0, 0.5, 0.0, 0.5))
				.drawFrame(4, 0, ColorF(1.0, 0.5, 0.0));
		}
	}
}

2. 囲碁の盤面

# include <Siv3D.hpp> // OpenSiv3D v0.4.3

namespace State
{
	constexpr int32 Empty = 0; // 石を置けるマス
	constexpr int32 Black = 1; // 黒が置かれているマス
	constexpr int32 White = 2; // 白が置かれているマス
}

Image CreateGobanImage(const Size& gobanSize)
{
	PerlinNoise noise;
	return Image::Generate(800, 800, [&](Point p)
	{
		const double x = Fraction(noise.octaveNoise0_1(p * Vec2(0.03, 0.0002), 2) * 25) * 0.15 + 0.85;
		return ColorF(x, 0.7 * Pow(x, 1.3), 0.35 * x);
	}).gaussianBlurred(1).scaled(gobanSize);
}

Image CreateStoneImage(bool black, int32 cellSize)
{
	const Vec4 sphere(0, 0, 1.5, 1.0);
	const Vec3 lightDir = Vec3(-1, 1, -1).normalized();

	return Image::Generate(200, 200, [=](Point p)
	{
		const Vec3 dir = Vec3(p.x / 100.0 - 1.0, p.y / -100.0 + 1.0, 1.0).normalized();
		const double B = -sphere.xyz().dot(dir);
		const double D = B * B - (sphere.xyz().dot(sphere.xyz()) - (sphere.w * sphere.w));

		if (D > 0.0)
		{
			const double t = -B - Sqrt(D);

			if (t > 0.0)
			{
				const Vec3 pos = dir * t;
				const Vec3 normal = (pos - sphere.xyz()).normalized();
				const Vec3 half = (lightDir - pos.normalized()).normalized();
				const double diffuse = Max(normal.dot(lightDir), 0.0) * (black ? 0.02 : 1.5);
				const double specular = Pow(Max(normal.dot(half), 0.0), 32);
				return ColorF(Pow(diffuse + specular, 1.0 / 2.2));
			}
		}

		return ColorF(black ? 0.0 : 0.5, 0.0);
	}).scaled(cellSize * 1.1 / 200);
}

constexpr Point GetPos(const Point& xy, const Point& gobanPos, int32 padding, int32 cellSize)
{
	return gobanPos + Point::All(padding) + (xy * cellSize);
}

void DrawGoban(const Texture& gobanTexture,
	const Point& gobanPos, const Size& gobanSize, int32 padding, int32 cellSize)
{
	RectF(gobanPos, gobanSize).drawShadow({ 6, 6 }, 20, 5);
	gobanTexture.draw(gobanPos);

	for (auto i : step(19))
	{
		const Point r0 = GetPos({ 0, i }, gobanPos, padding, cellSize);
		const Point c0 = GetPos({ i, 0 }, gobanPos, padding, cellSize);
		Rect(r0, 18 * cellSize, 1).draw(ColorF(0.1));
		Rect(c0, 1, 18 * cellSize).draw(ColorF(0.1));
	}

	for (auto x : { 3, 9, 15 })
	{
		for (auto y : { 3, 9, 15 })
		{
			const Point pos = GetPos({ x, y }, gobanPos, padding, cellSize);
			Circle(pos, 2.1).moveBy(0.5, 0.5).draw(ColorF(0.1));
		}
	}
}

void DrawStones(const Grid<int32>& board, const Point& gobanPos, int32 padding, int32 cellSize,
	const Texture& blackStoneTexture, const Texture& whiteStoneTexture)
{
	for (auto p : step(board.size()))
	{
		const int32 state = board[p];

		if (state == State::Empty)
		{
			continue;
		}

		const Vec2 pos = GetPos(p, gobanPos, padding, cellSize);
		Circle(pos, cellSize * 0.5).drawShadow({ 2.5, 2.5 }, 3);
		(state == State::Black ? blackStoneTexture : whiteStoneTexture)
			.drawAt(pos);
	}
}

Optional<Point> GetCellIndex(const Vec2& cursorPos, const Point& gobanPos, int32 padding, int32 cellSize)
{
	const int32 xIndex = static_cast<int32>((cursorPos.x - padding - gobanPos.x + (cellSize / 2)) / cellSize);
	const int32 yIndex = static_cast<int32>((cursorPos.y - padding - gobanPos.y + (cellSize / 2)) / cellSize);

	if (InRange(xIndex, 0, 18) && InRange(yIndex, 0, 18))
	{
		return Point(xIndex, yIndex);
	}
	else
	{
		return none;
	}
}

void Main()
{
	Scene::SetBackground(ColorF(0.7, 0.7, 0.5));
	constexpr Point gobanPos(30, 30);
	constexpr int32 padding = 18;
	constexpr int32 cellSize = 28;
	constexpr Size gobanSize = Size::All((18 * cellSize) + (2 * padding));
	const Texture gobanTexture(CreateGobanImage(gobanSize), TextureDesc::Mipped);
	const Texture blackStoneTexture(CreateStoneImage(true, cellSize), TextureDesc::Mipped);
	const Texture whiteStoneTexture(CreateStoneImage(false, cellSize), TextureDesc::Mipped);

	Grid<int32> board(Size(19, 19), State::Empty);
	board[{16, 3}] = State::Black;
	board[{2, 15}] = State::White;
	board[{4, 15}] = State::Black;
	board[{3, 3}] = State::White;
	board[{3, 13}] = State::Black;
	board[{14, 2}] = State::White;
	board[{1, 14}] = State::Black;
	board[{14, 16}] = State::White;

	while (System::Update())
	{
		DrawGoban(gobanTexture, gobanPos, gobanSize, padding, cellSize);
		DrawStones(board, gobanPos, padding, cellSize, blackStoneTexture, whiteStoneTexture);

		if (const Optional<Point> cellIndex = GetCellIndex(Cursor::Pos(), gobanPos, padding, cellSize))
		{
			Rect(gobanPos + Point::All(padding - (cellSize / 2) + 1) + (*cellIndex * cellSize), cellSize)
				.stretched(2)
				.draw(ColorF(0.5, 0.5, 1.0, 0.5))
				.drawFrame(4, 0, ColorF(0.5, 0.5, 1.0));
		}
	}
}