9. AI 応用 ②¶
9.1 OpenAI Vision API の活用¶
画像について AI に質問できる OpenAI の Vision API を活用して、描いた絵でしりとりをするゲームを作ります。描いたイラストを AI が判定します。
今日の #cppmix で発表した AI 絵しりとり!
— Ryo Suzuki (@Reputeless) June 14, 2024
描いた絵を AI が判定して、AI がわからなかったらゲームオーバー。#Siv3D pic.twitter.com/3IGEbZj9A4
ゲームのルール¶
- 指定されたアルファベット(例: A)から始まる言葉を考え、絵を描く。
- 描いた絵を AI が理解できれば OK, その言葉の最後の文字を使って次の言葉を考える。
開発をスムーズにするための設定¶
あらかじめパソコンの環境変数に API キーの文字列を登録しておくと、プログラム内に API キーを直接記述する必要がなくなります。
Windows での環境変数の設定方法
システムのプロパティから環境変数を設定します。
ユーザー環境変数に MY_OPENAI_API_KEY
という名前で API キーを設定します。システムに完全に適用させるためには、再起動が必要な場合があります。
macOS での環境変数の設定方法
ターミナルで次のようなコマンドを入力します。
launchctl setenv <環境変数のキー> "<環境変数の値>"
PC を再起動すると設定は失われます。
次のコードを実行すると、環境変数が正しく設定されているかを確認できます。Windows では、環境変数の適用のために PC の再起動が必要な場合があります。
# include <Siv3D.hpp>
void Main()
{
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
Print << API_KEY; // ここで API キーが表示されるはず
while (System::Update())
{
}
}
9.2 画面のサイズと背景¶
Window::Resize(幅, 高さ);
を使って画面のサイズを変更します。Scene::SetBackground(色);
を使って背景色を設定します。
コード
9.3 背景の市松模様を描画する¶
Rect{ x, y, size }.draw(color);
を使って正方形を並べます。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
while (System::Update())
{
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
}
}
9.4 ペイント用画像を用意する¶
- プログラムで加工可能な画像として
Image
クラスを使います。 - 画面に描画するテクスチャとして
DynamicTexture
クラスを使います。 DynamicTexture
はImage
から作成できます。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture)
{
texture.draw();
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
while (System::Update())
{
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// キャンバスを描く
DrawCanvas(texture);
}
}
9.5 キャンバスをいい感じに描く¶
RoundRect
クラスを使って角丸長方形を用意し、それに沿ってテクスチャを描画します。RoundRect
の.drawFrame(内側方向の太さ, 外側方向の太さ, 色)
を使って枠を描画します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
while (System::Update())
{
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// キャンバスを描く
DrawCanvas(texture, canvasPos);
}
}
9.6 ペイントする¶
- マウスの左ボタンが押されている間、画像に線を書き込みます。
Line{ from, to }.movedBy(-canvasPos).overwrite(image, thickness, color);
で線を描画します。.movedBy(-canvasPos)
は、キャンバスの位置と実際の画像上の位置を合わせるための処理です。texture.fill(image);
でDynamicTexture
の内容を新しい画像に更新します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// キャンバスを描く
DrawCanvas(texture, canvasPos);
}
}
9.7 キャンバスのクリアを実装する¶
SimpleGUI::Button(ラベル, 座標, 幅)
を使ってクリアボタンを配置します。- クリアボタンが押されたら、キャンバスをクリアします。
image.fill(color);
で画像を指定した色で塗りつぶします。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
}
}
9.8 お題の文字を描く¶
- お題の文字を表す変数
targetChar
を用意します。 - 文字を描画するために使うフォントを用意します。
font(文字またはテキスト).drawAt(サイズ、中心位置, 色);
を使って文字を描画します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
}
}
9.9 判定用の非同期タスクを準備する¶
- OpenAI のサーバーと非同期で通信するための
AsyncHTTPTask
クラスを用意します。 - 絵を AI に判定してもらうための「判定」ボタンを配置します。
- ボタンは、通信中には無効になるようにします。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
}
}
9.10 OpenAI API に送るリクエストを作成する¶
- OpenAI の Vision API に送るリクエスト
OpenAI::Vision::Request
を作成します。 .prompt
に、画像についての質問文を設定します。- 配列
.images
に、画像を適切な形式(ここでは Base64 エンコード)で追加します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
}
}
9.11 リクエストを送り、レスポンスを受け取る¶
OpenAI::Vision::CompleteAsync
で非同期タスクを作成します。- タスクが正常に完了したら、結果を取得します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// OpenAI API キー
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
// タスクを作成する
task = OpenAI::Vision::CompleteAsync(API_KEY, request);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// 非同期処理が完了し、正常なレスポンスである場合
if (task.isReady() && task.getResponse().isOK())
{
// 結果を取得する
const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();
// 結果を簡易表示する
Print << answer;
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
}
}
9.12 ゲームを進行させる¶
- 直近のしりとりの履歴を記録し、表示するようにします。
- 正解したら、次の文字に進むようにします。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}
void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
for (const 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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// OpenAI API キー
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
// 直近のしりとりの履歴を格納する配列
Array<String> recentWords = { String(1, targetChar) };
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
// タスクを作成する
task = OpenAI::Vision::CompleteAsync(API_KEY, request);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// 非同期処理が完了し、正常なレスポンスである場合
if (task.isReady() && task.getResponse().isOK())
{
// 結果を取得する
const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();
// 履歴の末尾を更新する
recentWords.back() = answer;
// 正解したら
if (answer != U"?")
{
targetChar = answer.back();
}
// 履歴に次の項目を追加する
recentWords << String(1, targetChar);
// 履歴の内容が 8 個より大きくなったら
if (8 < recentWords.size())
{
// 先頭の項目を削除する
recentWords.pop_front();
}
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
// しりとりの履歴を描く
DrawRecentHistory(recentWords, font);
}
}
9.13 履歴表示の改善¶
- 先頭の文字を強調表示します。
- 履歴が満杯のとき、最初の項目は画面からはみ出るようにします。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}
void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
// 履歴が満杯のときのあふれ処理
const double yOffset = (recentWords.size() < 8) ? 0 : -70;
for (const auto& [i, answer] : Indexed(recentWords))
{
// 1 文字目
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 });
// 1 文字目以降
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// OpenAI API キー
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
// 直近のしりとりの履歴を格納する配列
Array<String> recentWords = { String(1, targetChar) };
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
// タスクを作成する
task = OpenAI::Vision::CompleteAsync(API_KEY, request);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// 非同期処理が完了し、正常なレスポンスである場合
if (task.isReady() && task.getResponse().isOK())
{
// 結果を取得する
const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();
// 履歴の末尾を更新する
recentWords.back() = answer;
// 正解したら
if (answer != U"?")
{
targetChar = answer.back();
}
// 履歴に次の項目を追加する
recentWords << String(1, targetChar);
// 履歴の内容が 8 個より大きくなったら
if (8 < recentWords.size())
{
// 先頭の項目を削除する
recentWords.pop_front();
}
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
// しりとりの履歴を描く
DrawRecentHistory(recentWords, font);
}
}
9.14 スコアを表示する¶
- しりとりのスコアを記録し、表示します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}
void DrawRecentHistory(const Array<String>& recentWords, const Font& font)
{
// 履歴が満杯のときのあふれ処理
const double yOffset = (recentWords.size() < 8) ? 0 : -70;
for (const auto& [i, answer] : Indexed(recentWords))
{
// 1 文字目
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 });
// 1 文字目以降
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// OpenAI API キー
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
const Font font2 = Font{ FontMethod::MSDF, 40, Typeface::Heavy, FontStyle::Italic }.setBufferThickness(4);
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = U'C';
// 非同期タスク
AsyncHTTPTask task;
// 直近のしりとりの履歴を格納する配列
Array<String> recentWords = { String(1, targetChar) };
// スコア
int32 score = 0;
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
// タスクを作成する
task = OpenAI::Vision::CompleteAsync(API_KEY, request);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// 非同期処理が完了し、正常なレスポンスである場合
if (task.isReady() && task.getResponse().isOK())
{
// 結果を取得する
const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();
// 履歴の末尾を更新する
recentWords.back() = answer;
// 正解したら
if (answer != U"?")
{
targetChar = answer.back();
++score;
}
// 履歴に次の項目を追加する
recentWords << String(1, targetChar);
// 履歴の内容が 8 個より大きくなったら
if (8 < recentWords.size())
{
// 先頭の項目を削除する
recentWords.pop_front();
}
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
// しりとりの履歴を描く
DrawRecentHistory(recentWords, font);
// スコアを描く
DrawScore(score, font2);
}
}
9.15 細かい改善¶
- 最初のお題の文字をランダムに選ぶようにします。
- AI からのレスポンスの待機中に、回転するリングを表示します。
コード
# include <Siv3D.hpp>
void DrawCheckerboard(int32 size, const ColorF& color)
{
// 縦横のマス目の数
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)
{
// (x + y) が偶数のときだけ正方形を描く
if (IsEven(x + y))
{
Rect{ (x * size), (y * size), size }.draw(color);
}
}
}
}
void DrawCanvas(const Texture& texture, const Point& canvasPos)
{
// 角丸長方形
const RoundRect rrect{ canvasPos, texture.size(), 20 };
// 角丸長方形に沿ってペイント結果を描く
rrect(texture).draw();
// 角丸長方形の枠を描く
rrect.drawFrame(1, 15, ColorF{ 0.6, 0.4, 0.2 });
}
void DrawTargetCharacter(char32 targetChar, const Point& canvasPos, const Font& font)
{
// お題表示用の円
const Circle circle{ canvasPos.movedBy(30, 30), 70 };
// 円を描く
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 });
// お題の文字を描く
font(targetChar).drawAt(70, circle.center, ColorF{ 0.1 });
}
void DrawRecentHistory(const Array<String>& recentWords, const Font& font, bool isWaiting)
{
// 履歴が満杯のときのあふれ処理
const double yOffset = (recentWords.size() < 8) ? 0 : -70;
for (const auto& [i, answer] : Indexed(recentWords))
{
// 1 文字目
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 });
// 1 文字目以降
font(answer.substr(1)).draw(46, Vec2{ 736, (47 + i * 80 + yOffset) }, ColorF{ 0.1 });
// レスポンス待機中のとき
if (isWaiting)
{
// 最後の文字の周りに回転するリングを描く
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);
// テクスチャの内容を更新する
texture.fill(image);
}
}
void ClearCanvas(Image& image, DynamicTexture& texture, const Color& color)
{
image.fill(color);
// テクスチャの内容を更新する
texture.fill(image);
}
void Main()
{
// ウィンドウを 1280x720 にリサイズする
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
// OpenAI API キー
const String API_KEY = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
// フォントを用意する
const Font font{ FontMethod::MSDF, 40, Typeface::Heavy };
const Font font2 = Font{ FontMethod::MSDF, 40, Typeface::Heavy, FontStyle::Italic }.setBufferThickness(4);
// キャンバスの左上の位置
const Point canvasPos{ 100, 60 };
// キャンバスのサイズ
const Size canvasSize{ 512, 512 };
// ペイント用の画像
Image image{ canvasSize, Palette::White };
// ペイント用の画像からテクスチャを作成する
DynamicTexture texture{ image };
// お題の文字
char32 targetChar = Random(U'A', U'Z');
// 非同期タスク
AsyncHTTPTask task;
// 直近のしりとりの履歴を格納する配列
Array<String> recentWords = { String(1, targetChar) };
// スコア
int32 score = 0;
while (System::Update())
{
// ペイントを行う
PaintCanvas(image, canvasPos, texture, 6, ColorF{ 0.0 });
// 背景の市松模様を描く
DrawCheckerboard(40, ColorF{ 0.55, 0.75, 0.65 });
// 送信ボタンが押されたら
if (SimpleGUI::Button(U"判定", Vec2{ (canvasPos.x + 100), 620 }, 120,
(not task.isDownloading()))) // 判定結果待機中のとき以外、ボタンを有効にする
{
// プロンプト
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.";
// リクエスト
OpenAI::Vision::Request request;
// リクエストにプロンプトを設定
request.questions = prompt;
// リクエストに画像を添付
request.images << OpenAI::Vision::ImageData::Base64FromImage(image);
// タスクを作成する
task = OpenAI::Vision::CompleteAsync(API_KEY, request);
}
// クリアボタンが押されたら
if (SimpleGUI::Button(U"クリア", Vec2{ (canvasPos.x + canvasSize.x - 220), 620 }, 120))
{
// キャンバスをクリアする
ClearCanvas(image, texture, Palette::White);
}
// 非同期処理が完了し、正常なレスポンスである場合
if (task.isReady() && task.getResponse().isOK())
{
// 結果を取得する
const String answer = OpenAI::Vision::GetContent(task.getAsJSON()).uppercase();
// 履歴の末尾を更新する
recentWords.back() = answer;
// 正解したら
if (answer != U"?")
{
targetChar = answer.back();
++score;
}
// 履歴に次の項目を追加する
recentWords << String(1, targetChar);
// 履歴の内容が 8 個より大きくなったら
if (8 < recentWords.size())
{
// 先頭の項目を削除する
recentWords.pop_front();
}
}
// キャンバスを描く
DrawCanvas(texture, canvasPos);
// お題の文字を描く
DrawTargetCharacter(targetChar, canvasPos, font);
// しりとりの履歴を描く
DrawRecentHistory(recentWords, font, task.isDownloading());
// スコアを描く
DrawScore(score, font2);
}
}