Skip to content

68. Project: AI Shiritori (Word Chain Game)

Create a word chain game using drawings by leveraging OpenAI's Vision API. The AI judges the drawn illustrations.

OpenAI API Key Required

  • Completing the program in this chapter requires the OpenAI API key obtained in Tutorial 67

68.1 Game Rules

  • Think of a word that starts with the specified alphabet (e.g., A) and draw a picture
  • If the AI can understand the drawn picture, it's OK. Use the last letter of that word to think of the next word

Completed Image (Click to Play)

68.2 Screen Size and Background

  • Set the screen size and background color

Code
# include <Siv3D.hpp>

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{

	}
}

68.3 Background Checkerboard Pattern

  • Draw a background checkerboard pattern by arranging squares

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{
		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
	}
}

68.4 Paint Image

  • Prepare editable image data Image for painting
  • Prepare DynamicTexture for drawing that image to the scene

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture)
{
	texture.draw();
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	while (System::Update())
	{
		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// Draw canvas
		DrawCanvas(texture);
	}
}

68.5 Canvas

  • Use the RoundRect class to prepare a rounded rectangle and draw the painted texture along it
  • Use RoundRect's .drawFrame(inner thickness, outer thickness, color) to draw the canvas frame

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	while (System::Update())
	{
		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// Draw canvas
		DrawCanvas(texture, canvasPos);
	}
}

68.6 Painting

  • While the left mouse button is pressed, draw lines on the image
  • Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color); draws lines on the image
  • .movedBy(-canvasPos) is processing to align the canvas position with the actual image position
  • texture.fill(image); updates the DynamicTexture content with the new image

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// Draw canvas
		DrawCanvas(texture, canvasPos);
	}
}

68.7 Canvas Clear

  • Create an image clear button using SimpleGUI
  • Clear the canvas when the clear button is pressed
  • image.fill(color); fills the image with the specified color

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);
	}
}

68.8 Topic Character

  • Prepare a variable targetChar to represent the topic character
  • Prepare a font to use for drawing characters
  • Use font(character or text).drawAt(size, center position, color); to draw characters

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);
	}
}

68.9 Communication Setup

  • Prepare AsyncHTTPTask class for asynchronous communication with OpenAI server
  • Place a "Judge" button for having the AI judge the drawing
  • Make the judge button disabled during communication
Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{

		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);
	}
}

68.10 Creating Requests

  • Create a request OpenAI::Vision::Request to send to OpenAI's Vision API
  • Add images to the array .images
  • Set the question text about the image in .prompt
Prompt Japanese Translation
What is drawn in the image? The answer starts with the letter "{}".
Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.
Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in the image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);


		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);
	}
}

68.11 AI Interaction

  • Create an asynchronous task with OpenAI::Vision::CompleteAsync
  • When the task completes successfully, get the result in uppercase

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// OpenAI API key
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in this image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);

			// Create task
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// When asynchronous processing is complete and response is OK
		if (task.isReady() && task.getResponse().isOK())
		{
			// Get result
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// Simple display of result
			Print << answer;
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);
	}
}

68.12 Game Progress

  • Record and display recent word chain history
  • Proceed to the next character when correct

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
	for (auto&& [i, answer] : Indexed(recentWords))
	{
		font(answer).draw(46, Vec2{ 736, (47 + i * 80) }, ColorF{ 0.1 });
	}
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// OpenAI API key
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	// Array to store recent word chain history
	Array<String> recentWords = { String(1, targetChar) };

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in this image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);

			// Create task
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// When asynchronous processing is complete and response is OK
		if (task.isReady() && task.getResponse().isOK())
		{
			// Get result
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// Update end of history
			recentWords.back() = answer;

			// If correct
			if (answer != U"?")
			{
				targetChar = answer.back();
			}

			// Add next item to history
			recentWords << String(1, targetChar);

			// If history contains more than 8 items
			if (8 < recentWords.size())
			{
				// Remove first item
				recentWords.pop_front();
			}
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);

		// Draw word chain history
		DrawRecentHistory(recentWords, font);
	}
}

68.13 Improving History Display

  • Emphasize the first character
  • When history is full, let the first item go off screen

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
	// Overflow handling when history is full
	const double yOffset = (recentWords.size() < 8) ? 0 : -70;

	for (auto&& [i, answer] : Indexed(recentWords))
	{
		// First character
		const Vec2 pos{ 700, (80 + i * 80 + yOffset) };
		Circle{ pos, 32 }.draw(ColorF{ 0.8, 0.9, 1.0 });
		font(answer.front()).drawAt(46, pos, ColorF{ 0.1 });

		// Characters after first
		font(answer.substr(1)).draw(46, Vec2{ 736, (47 + i * 80 + yOffset) }, ColorF{ 0.1 });
	}
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// OpenAI API key
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	// Array to store recent word chain history
	Array<String> recentWords = { String(1, targetChar) };

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in this image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);

			// Create task
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// When asynchronous processing is complete and response is OK
		if (task.isReady() && task.getResponse().isOK())
		{
			// Get result
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// Update end of history
			recentWords.back() = answer;

			// If correct
			if (answer != U"?")
			{
				targetChar = answer.back();
			}

			// Add next item to history
			recentWords << String(1, targetChar);

			// If history contains more than 8 items
			if (8 < recentWords.size())
			{
				// Remove first item
				recentWords.pop_front();
			}
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);

		// Draw word chain history
		DrawRecentHistory(recentWords, font);
	}
}

68.14 Score

  • Record and display score

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
	// Overflow handling when history is full
	const double yOffset = (recentWords.size() < 8) ? 0 : -70;

	for (auto&& [i, answer] : Indexed(recentWords))
	{
		// First character
		const Vec2 pos{ 700, (80 + i * 80 + yOffset) };
		Circle{ pos, 32 }.draw(ColorF{ 0.8, 0.9, 1.0 });
		font(answer.front()).drawAt(46, pos, ColorF{ 0.1 });

		// Characters after first
		font(answer.substr(1)).draw(46, Vec2{ 736, (47 + i * 80 + yOffset) }, ColorF{ 0.1 });
	}
}

void DrawScore(int32 score, const Font& font)
{
	const Vec2 center = font(score).region(140, Arg::topRight(1185, 15)).center();
	font(score).draw(TextStyle::OutlineShadow(0.2, ColorF{ 1.0 }, Vec2{ 2, 2 }, ColorF{ 0.0, 0.5 }), 140,
		Arg::topRight(1185, 15), ColorF{ 1.0, 0.6, 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// OpenAI API key
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
	const Font font2 = Font{ FontMethod::MSDF, 40, Typeface::Heavy, FontStyle::Italic }.setBufferThickness(4);

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = U'C';

	// Asynchronous task
	AsyncHTTPTask task;

	// Array to store recent word chain history
	Array<String> recentWords = { String(1, targetChar) };

	// Score
	int32 score = 0;

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in this image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);

			// Create task
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// When asynchronous processing is complete and response is OK
		if (task.isReady() && task.getResponse().isOK())
		{
			// Get result
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// Update end of history
			recentWords.back() = answer;

			// If correct
			if (answer != U"?")
			{
				targetChar = answer.back();
				++score;
			}

			// Add next item to history
			recentWords << String(1, targetChar);

			// If history contains more than 8 items
			if (8 < recentWords.size())
			{
				// Remove first item
				recentWords.pop_front();
			}
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);

		// Draw word chain history
		DrawRecentHistory(recentWords, font);

		// Draw score
		DrawScore(score, font2);
	}
}

68.15 Minor Improvements

  • Make the first topic character randomly selected
  • Display a rotating ring while waiting for AI response

Code
# include <Siv3D.hpp>

void DrawCheckerboard(int32 size, const ColorF& color)
{
	// Number of horizontal and vertical cells
	const int32 yCount = (720 / size + 1);
	const int32 xCount = (1280 / size + 1);

	for (int32 y = 0; y < yCount; ++y)
	{
		for (int32 x = 0; x < xCount; ++x)
		{
			// Draw square only when (x + y) is even
			if (IsEven(x + y))
			{
				Rect{ (x * size), (y * size), size }.draw(color);
			}
		}
	}
}

void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
	// Rounded rectangle
	const RoundRect rrect{ canvasPos, texture.size(), 20 };

	// Draw paint result along the rounded rectangle
	rrect(texture).draw();

	// Draw rounded rectangle frame
	rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}

void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
	// Circle for topic display
	const Circle circle{ canvasPos.movedBy(30, 30), 70 };

	// Draw circle
	circle.drawShadow(Vec2{ 2, 2 }, 12, 2, ColorF{ 0.2, 0.4, 0.3, 0.5 })
		.draw(ColorF{ 0.8, 0.9, 1.0 })
		.stretched(-1.5).drawFrame(1, ColorF{ 1.0 });

	// Draw topic character
	font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}

void DrawRecentHistory(const Array<String>& recentWords, const Font& font, bool isWaiting)
{
	// Overflow handling when history is full
	const double yOffset = (recentWords.size() < 8) ? 0 : -70;

	for (auto&& [i, answer] : Indexed(recentWords))
	{
		// First character
		const Vec2 pos{ 700, (80 + i * 80 + yOffset) };
		Circle{ pos, 32 }.draw(ColorF{ 0.8, 0.9, 1.0 });
		font(answer.front()).drawAt(46, pos, ColorF{ 0.1 });

		// Characters after first
		font(answer.substr(1)).draw(46, Vec2{ 736, (47 + i * 80 + yOffset) }, ColorF{ 0.1 });

		// When waiting for response
		if (isWaiting)
		{
			// Draw rotating ring around the last character
			if (i == recentWords.size() - 1)
			{
				Circle{ pos, 42 }.drawArc((Scene::Time() * 240_deg), 300_deg, 5, 2, ColorF{ 0.8, 0.9, 1.0 });
			}
		}
	}
}

void DrawScore(int32 score, const Font& font)
{
	const Vec2 center = font(score).region(140, Arg::topRight(1185, 15)).center();
	font(score).draw(TextStyle::OutlineShadow(0.2, ColorF{ 1.0 }, Vec2{ 2, 2 }, ColorF{ 0.0, 0.5 }), 140,
		Arg::topRight(1185, 15), ColorF{ 1.0, 0.6, 0.1 });
}

void PaintCanvas(Image& image, const Point& canvasPos, DynamicTexture& texture, int32 thickness, const ColorF& color)
{
	if (MouseL.pressed())
	{
		const Point from = (MouseL.down() ? Cursor::Pos() : Cursor::PreviousPos());
		const Point to = Cursor::Pos();
		Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);

		// Update texture content
		texture.fill(image);
	}
}

void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
	image.fill(color);

	// Update texture content
	texture.fill(image);
}

void Main()
{
	// Resize window to 1280x720
	Window::Resize(1280, 720);

	// Set background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// OpenAI API key
	const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");

	// Prepare font
	const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
	const Font font2 = Font{ FontMethod::MSDF, 40, Typeface::Heavy, FontStyle::Italic }.setBufferThickness(4);

	// Canvas top-left position
	const Point canvasPos{ 100, 60 };

	// Canvas size
	const Size canvasSize{ 512, 512 };

	// Paint image
	Image image{ canvasSize, Palette::White };

	// Create texture from paint image
	DynamicTexture texture{ image };

	// Topic character
	char32 targetChar = Random(U'A', U'Z');

	// Asynchronous task
	AsyncHTTPTask task;

	// Array to store recent word chain history
	Array<String> recentWords = { String(1, targetChar) };

	// Score
	int32 score = 0;

	while (System::Update())
	{
		// Perform painting
		PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });

		// Draw background checkerboard pattern
		DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });

		// When send button is pressed
		if (SimpleGUI::Button(U"Judge", Vec2{ (canvasPos.x + 100), 620 }, 120,
			(not task.isDownloading()))) // Enable button when not waiting for judgment result
		{
			// Prompt
			String prompt = U"What is drawn in this image? The answer starts with the letter {}. "_fmt(targetChar);
			prompt += U"Write only the answer. Commas and periods are prohibited. If you don't know, output only a question mark.";

			// Request
			OpenAI::Vision::Request request;

			// Set prompt to request
			request.questions = prompt;

			// Attach image to request
			request.images << OpenAI::Vision::ImageData::Base64FromImage(image);

			// Create task
			task = OpenAI::Vision::CompleteAsync(API_KEY, request);
		}

		// When clear button is pressed
		if (SimpleGUI::Button(U"Clear", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
		{
			// Clear canvas
			ClearCanvas(image, texture, Palette::White);
		}

		// When asynchronous processing is complete and response is OK
		if (task.isReady() && task.getResponse().isOK())
		{
			// Get result
			const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();

			// Update end of history
			recentWords.back() = answer;

			// If correct
			if (answer != U"?")
			{
				targetChar = answer.back();
				++score;
			}

			// Add next item to history
			recentWords << String(1, targetChar);

			// If history contains more than 8 items
			if (8 < recentWords.size())
			{
				// Remove first item
				recentWords.pop_front();
			}
		}

		// Draw canvas
		DrawCanvas(texture, canvasPos);

		// Draw topic character
		DrawTargetCharacter(targetChar, canvasPos, font);

		// Draw word chain history
		DrawRecentHistory(recentWords, font, task.isDownloading());

		// Draw score
		DrawScore(score, font2);
	}
}