3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jev にゲームをプレイさせてみたい!

3
Posted at

はじめに

 こんにちは、yu_Matsuです!今回は、最近話題となっている Jev が画像に対応したとのことで、Jev にゲームをプレイさせてみたい と思ったため、試してみました。気軽に始めたつもりが、意外と考察しがいのある結果が得られたので、記事にしたいと思います!

1. Jevとは

 Jev は TypeSafe AI が2026年9月15日に公開した「文章を書かず、判断だけを返す」AI モデルです(すでにさまざまな記事で解説されているので、本記事ではある程度省略します)。判断する材料(state)とそれに対する質問(questions)を渡すことで、その判断結果を確率と確信度付きで返します。チャットやコード生成には使えず、分類や振り分け、採点、検証などに利用するモデルで、公式はこれを LLM とは別カテゴリの System One モデル と呼んでいます。
 とにかくその速さと安さが話題となっており、LLMと組み合わせる形での利用が広がってきています。

 Jev への質問の仕方としては以下の3種類があり、ユースケースにより使い分けます。

  • noul:はい/いいえで答える質問。Jev からは「はい」である確率(0〜1)が返る
  • choice:候補から 1 つ選ぶ質問。候補ごとの確率・確信度が返る
  • score:順序のある段階で採点する質問。段階ごとの確率・確信度と、それを重みにした平均スコアが返る

 公開当初は、判断する材料(state)として利用できるのはテキストのみでしたが、10 月 14 日に公開された jev-1.14.0 では、画像に対応しました。なんと、この画像入力もテキストの場合と料金が変わらないというのだから驚きです。まだ実験機能(公式の呼び名は Danger Zone)で、公式も本番向けではないとしていますが、今回の記事ではこの画像入力利用して、「Jev がゲームをプレイできるかどうか」を試してみます。

2. 用意したゲーム

 Jev にプレイさせるゲームとして、以下のような簡単な 強制横スクロールのゲーム を用意しました。緑の四角が自機で、右方向へ勝手に走り続けます。終了条件は自機が障害物にぶつかることのみで、ゴールは特になく、画面下の生存時間、通過した障害物の本数がスコアになります。

その他詳細な仕様は以下になります。

  • 障害物は低い柱・高い柱・天井の 3 種類です。天井は上から出ている障害物で、ジャンプするとぶつかるのでくぐる(ジャンプをしない)必要があります

  • 自機は一定の速さで走ります。自機の右端から障害物の左端までを「障害物までの距離」と呼ぶことにします。これが 20〜40 px になったところでジャンプすれば障害物を越えられるようになっています。(px はゲーム画面(幅 400 px)の中での値です)

  • ゲームの速度レベルは倍率で変更できるようにしています。(レベル 1 で 160 px/秒、レベル 2 で 320 px/秒)

  • 今回の記事では一部の検証でしか出番がありませんが、コースも複数用意しています

3. 実際にJevにプレイさせてみる

 Jevにゲームをプレイさせる方法として、まずは単純なものを考えてみました。プレイ中の画像をJevに都度渡して、「自機が障害物に近ければ、noul形式でジャンプするかどうかを判断させる」というものです。

3-1. 実際のJevへの問い合わせ方

 ゲーム画面(400×320)をそのまま Jev に渡して、「今ジャンプすべきかどうか」を noul形式 で 1 回だけ問い合わせます。noul は、はい/いいえで答えられる質問を投げて、「はい」の確率(0〜1)を返す形式であり、Jevの回答が 0.6 以上 であれば、ジャンプすると判断したことになります。

サンプルコード
const res = await client.systemOne({
  state: image(png),
  questions: {
    jump: noul("Should the green square jump right now to avoid the red obstacle?"),
  },
});

if (res.answers.jump.noul >= 0.6) jump();

 ゲームはブラウザで動くため、ブラウザからゲーム画面を画像としてJev用のバックエンドサーバーに送り、Jevの回答をブラウザに返します。ブラウザからサーバーに画像を送ってからJevの答えが届くまでは、筆者の環境でだいたい 170 ms くらいでした。
 そのため、ゲーム画面をサーバーに送る間隔のデフォルト値は、この往復時間より少し長い 200 ms にしました。こうすることで、基本的にはJevの回答がブラウザに届いてから次の画面を送ることになり、画面を 1 枚送るごとに回答が 1 つ返ってきます。

 ゲーム側は、Jevが算出した確率をしきい値と比較して超えていればジャンプ処理を行うだけです。障害物がどこにあるか、どれくらい近いかは知りません。「ジャンプするかどうか」の判断を、丸ごと Jev に任せる形なるので、擬似的に Jev がプレイしている状況を再現できます。

3-2. 実際に動かしてみた結果

1a.gif

 結果としては、障害物を1本も超えられませんでした...。 GIF画像下に出ているバーが Jev の回答(跳ぶべきか)を可視化していて、縦の目盛りがジャンプを判断するしきい値の 0.6 を表しています。障害物(柱)に近づいても、バーは 0.4〜0.5 のあたりから動きません。目盛りに届かないまま、柱にぶつかってしまいます。

3-3. 何がダメだったかを考察/検証する

 今のままだと1つも障害物を越えられずゲームが終了するため、Jev がゲーム中の各画面でどのように回答しているのかがよく分かりません。そこで、ゲーム中の様々な画面を集めて、1 枚ずつ Jev に問い合わせてみました。

  1. 画面を集める:ゲームを自動操作して画面を進めます。自動操作は自機から障害物の位置を直接把握してジャンプするので、失敗することはありません。実際に 3 コースで記録した結果、画像は 387 枚となりました
  2. 自機から障害物までの距離も記録:0.15 秒ごとに画面を保存し、そのときの自機から障害物までの距離も記録します
  3. Jev に問い合わせる:保存した画面を 1 枚ずつ Jev に送り、実際のプレイのときと同じように「今ジャンプするべきか」を問い合わせます
  4. 集計:自機から障害物までの距離ごとに分けて、Jev の回答の確率を平均します
     
    検証の結果は以下になります。
障害物までの距離 〜20 px 20〜45 px
(ジャンプの適正)
45〜60 px 60〜100 px 100〜200 px 200 px〜 天井
Jev の回答の平均 0.39 0.50 0.40 0.40 0.36 0.34 0.66

結果から読み取れることとしては、次の2点になります。

  • 障害物までの距離が変わっても、Jevの回答はほとんど変わらない。 遠くても近くても 0.3〜0.4 台です。ジャンプと判断すべきである 20〜45 px 帯でも 0.5 しかなく、0.6 には届きません
  • 障害物(天井)のほうが、障害物(柱)より高い。 天井は上から出ていて、柱と比べて大きく目立つので、「赤いものが目の前にある → ジャンプする」と判断している可能性が高いです

 Jev は「赤い障害物があるから、ジャンプしたほうがいいかも」くらいには判断していますが、「もうすぐぶつかる」や「まだ遠い」といった距離の判断は出来ていません。

3-4. 障害物に関する「見本」を渡してみる

 障害物からの距離にも、障害物(天井)にも反応しないということは、「ジャンプするべきか」がどういう状況を指しているのか、Jev に伝わっていないのかもしれません。人間でも、初めてプレイするゲームはどのようにプレイするかの「見本」がないとなかなかうまくプレイできないと思いますが、これは Jev にも当てはまります。そこで、Jev にも「見本」を渡してみます。

 Jev には、質問文とは別に criteria を渡すことができ、これが「見本」に相当します。公式ドキュメントでは、noul の criteria は 「何を『はい』とし、何を『いいえ』とするか」を伝えるためのもの(to say what counts as a yes and what counts as a no) と説明されています。

 今回の場合だと、「ジャンプすべきか」の判断に関して、自機から障害物までどれくらいの距離なのかは、質問文から読み取ることはできません。criteria を使うと、障害物が「目の前にあるとき」はジャンプすべき、障害物が「まだ遠いとき」「天井の場合」はジャンプすべきでない、といったように、何を持って判断すべきかの付加情報を Jev に渡すことができます。

 criteria 自体は前からありますが、Jev v1.14.0 で画像も送れるようになったため、これをフル活用したいと思います。
 noul形式の場合の criteria には、true と false の 2 つの欄があります。

  • true:「ジャンプすべきか」に「はい(ジャンプする)」と回答するのは、どんな画面のときか
  • false:「ジャンプしない」と回答するのは、どんな画面のときか

今回は、それぞれの欄に短い説明文と見本の画面を入れました。true にはジャンプすべき状況の画面(障害物が「目の前にあるとき」)を 1 枚、false にはジャンプしてはいけない状況の画面(障害物が「まだ遠いとき」「天井の場合」)を 2 枚用意しています。

見本 中身 ねらい
true 低い柱まで 30 px(3 節の図と同じ画面) 障害物が近いときはジャンプする
false 低い柱まで、まだ 200 px ある 障害物が遠いときはジャンプしない
false 障害物(天井)まで 30 px 天井は近くてもジャンプしない(くぐる)

Jevへの指示のサンプルコードは以下になります。withAssets は、テンプレートリテラルの中に画像を埋め込む書き方です。それぞれの見本画像には、短い説明文も添えています(コード内の${xxx}に見本画像が入ります)。

jump: noul("Should the green square jump right now to avoid the red obstacle?", {
  true: withAssets`Yes: the red obstacle is just ahead of the green square, like this: ${YES}`,
  false: withAssets`No: the red obstacle is still far away, like this: ${NO_FAR}. Also no when the obstacle hangs down from the top, like this: ${NO_CEILING}`,
}),

3-5. 「見本」導入の効果が一定あった

 見本画像を追加した状態で、実際にゲームを動かしてみました。その結果が以下になります。ゲームの速度レベルや閾値は変えていません。

2.gif

 今度は障害物を判断してジャンプしてくれるようにはなりました! 障害物(柱)に近づくと、画面下の「跳ぶべきか」のバーが伸びて閾値を超えて緑になり、ジャンプしています。また、天井の障害物ではバーは伸びず、ジャンプせずに潜っています。4つ目の障害物にぶつかってしまっていますが、明らかに進歩がありました。

 3-3で実施した検証を今回も同様に行なって比較してみました。結果が以下になります。

障害物までの距離 〜20 px 20〜45 px
45〜60 px 60〜100 px 100〜200 px 200 px〜 天井
見本なし(3-3再掲) 0.39 0.50 0.40 0.40 0.36 0.34 0.66
見本あり 0.34 0.75 0.72 0.45 0.10 0.08 0.12

 自機が障害物(柱)から遠いときは 0.1 前後まで下がり、近づくにつれて上昇、ジャンプ適正距離で 0.75 になっていることがわかります。また、障害物(天井)の場合は 0.66 から 0.12 になり、「ジャンプすべきではない障害物」と認識できるようになりました。

3-6. ジャンプできるようになったがすぐにゲームオーバーになる

 障害物の認識精度は上がりましたが、依然として、少し進むと障害物にぶつかってしまいます。こちらに関して少し考察しました。
 「Jev が回答の判断に利用した画面」での自機から障害物までの距離と、「Jevの回答が届いて実際にジャンプしたとき」の自機から障害物までの距離を、サーバーのログから取得して比較してみました。

障害物 Jevが回答の判断に用いた画面 回答が届いて実際にジャンプしたときの画面 結果
1 本目(柱) 48 px 約 21 px 越えた(ぎりぎり)
2 本目(柱) 56 px 約 29 px 越えた
3 本目(天井) - - ジャンプせずにくぐった
4 本目(柱) 41 px 約 14 px ぶつかった

 Jev の判断自体はどれも問題がないように見えていて、ジャンプするべきタイミングの少し手前で「ジャンプする」と回答しています。問題は、その回答がブラウザに届くまでの時間です。

  • Jev が判断に用いる画面は、ブラウザ側で画面を送ったときの様子
  • 実際にブラウザ側でジャンプするのは、Jevからの回答が届いたとき
  • その間(約 170 ms)に、自機は約 25〜30 px 進んでいる

 例えば障害物一つ目の場合、自機から障害物までの距離が48 px で「ジャンプする」と Jev が判断しても、実際にジャンプする際は自機と障害物の距離は約 21 px まで縮まっています。同じようなことが4つ目の障害物でも起こっていて、実際にジャンプをする際は障害物から約 14 px のところまで自機が来てしまい、ジャンプしてもう間に合いません。実際にgif画像を見てみると、ジャンプはしようとしているが、跳び越えられていないことがわかります。
 ジャンプで跳べる範囲の幅は、ゲームの仕様上 20 px しかありません。ブラウザとJevとのやり取りの往復のあいだに自機が走る距離は、それより長く、Jev の回答がゲームに反映されるまでにラグが生じてしまっているということになります。

 この問題を解決するためには、ラグを考慮する必要があるのですが、こちらに関しては Jev だけで解決するのは厳しいそうです(できないわけではないと思いますが...)。なので、少し考え方を変えてみました。

4. Jevを利用したオートプレイの形に落とし込んでみる

 Jevを利用する際に発生するラグを解消するために、その補正を行うことを考えてみました。補正を行うには、ゲーム側でジャンプ処理を実行する際に、障害物の位置を知っている必要があります。そこで、Jev とゲームの処理で役割を分けることにしました。

  • Jev:画面を見て、障害物がどこにあるか、どんな種類かを判断する
  • ゲームの処理:Jevの判断をもとに、今自機がどこにあるかを計算し、いつジャンプするかを決める

実際にジャンプの判断をする役割がゲームの処理に移り、「Jevがゲームをする」という前提は崩れてしまったので、「Jevを利用したオートプレイ」という考え方にシフトします。

roles-b.png

4-1. Jevへの問い合わせ方

 Jevに障害物の「位置」を問い合わせるために、box という質問形式を使います。これは、Jev v1.14.0 で画像対応したのと同時に追加されたものになります。公式ドキュメントでは、「画像の中で、何かがどこにあるかを見つける質問」(for finding where something is in an image)と説明されています。
 Jevの回答形式は、「検出したものを囲む矩形情報」と「確信度(confidence)」になります。矩形情報は、上下左右の4辺の位置情報となっており、いずれも画像の左上から測った画像の幅・高さに対する「割合(0〜1)」です。確信度は、choice形式やscore形式の場合と同じで、Jev が自身の回答にどれくらい自信があるかを 0〜1 で表したものになります。

 実際の問い合わせのサンプルコードが以下になります。obstacleで「box」を指定し、括弧内で特徴を説明しています。先ほどの方法では「ジャンプするべきか」の確率だけだったので、障害物までの距離が分かりませんでした。box なら障害物の「位置」が返ってくるので、コードが距離を計算して、ラグの補正に利用できます。

const res = await client.systemOne({
  state: image(png), // 自機より右の 320×320 だけ切り出した画面
  questions: {
    obstacle: box("The red obstacle"),
    is_ceiling: noul("Does the leftmost red obstacle hang down from the top of the image?"),
  },
});

const { box: b, confidence } = res.answers.obstacle;
// b.left, b.top, b.right, b.bottom は画像に対する割合(0〜1)

Jev に送る画像の工夫
 
Jev に送る画面画像には工夫が 2 つあります。

  • 自機を写さない。 画面全体を送ると、box が左にある自機のほうに引っぱられることがあるため、画面のうち自機より右の 320×320 だけを切り出して送ります
  • box は 1 つしか返らない。 公式ドキュメントにも、回答としてはいつも矩形 1 つが返るとあります。障害物が 2 つ見えていても、返ってくるのはそのうち 1 つです。たいていは一番手前(左)のものを返すので、それを「次に越える障害物」として扱います

4-2. ゲーム側の処理

 Jevからの回答が届いたら、ゲーム側では次の順に処理を行います。

① 障害物があるかどうかの確認

 box は、画像内に障害物が見つからない場合でも None 等の空情報が返るのではなく、何かしらの矩形情報を返します。そこで、box の確信度が 0.25 未満なら、「障害物なし」とみなすようにします。実際に検証の際に試してみたところ、障害物がない画面では 0.11〜0.13、ある画面では 0.3 以上だったので、その間で設定しました。

② 障害物が天井かどうかの確認

 box の上端が、画像の上から 4 割より上にあれば障害物を天井とみなします。天井は画面の一番上から出ているものであり、その上端は必ず画面の上端に接しています。そのため、障害物の上端の位置で天井か柱かを判断することができます。

 is_ceiling の noul も同様に障害物の種類を問い合わせていますが、こちらは判断には使っていません。画面に障害物が 2 つ以上あると、手前ではなく奥のほうについて回答することがあったからです。box と noul で見ている障害物がずれると困るので、種類も box から決めることにしています。(noul 側の回答はログ用に残している)

③ 障害物の今の位置を見積もる

 Jev が回答した障害物の位置は、画面を送ったときの位置です。自機は一定の速さで移動するので、答えが届くまでの間にも障害物に近づいているので、今まで言及してきたラグが発生します。そこで、画面を送ったときのゲーム内の時刻を保持しておき、Jevの回答が届いたら、その間に自機が移動した分を引きます。移動する速さはいつも同じ(ゲーム内の時間で 160 px/秒)なので、引き算だけで済みます。(言葉ではイマイチ伝わらないと思うので、計算式を下に掲載しています)

今の障害物までの距離 = Jev が見た障害物の左端
                   − 160 ×(今の時刻 − 画面を送った時刻)
                   − 自機の右端

時刻はゲーム内の時刻で数えるので、ゲームの速度レベルを 2 にしても式は変わりません(ゲーム内の時刻が 2 倍の速さで進むだけです)。

④ ジャンプを予約する

 今の障害物までの距離から 30 px を引いて、自機がそこまで走る時間を計算し、その時間後にジャンプを予約します。

ジャンプまでの時間 =(今の障害物までの距離 − 30)÷ 160

 30 px は、ジャンプの範囲 20〜40 px の中央になります。この範囲の値は、ゲームの正解の座標を使って、テスト用の操作者で検証した結果を用いています(20〜40 px でジャンプすれば、どの障害物も越えられる)。

+αの細かい工夫

  • 同じ障害物は、何回か検出した結果をまとめる。 box の位置は 1 回の検出ごとに数 px〜十数 px ぶれます。自機は一定の速さで走るので、検出した位置を「ゲーム内の時刻 0 のときの位置」に直すと、同じ障害物なら本来同じ値になります。そこで、同じ障害物については複数回検出した位置の中央値を使います。以前の値から 30 px 以上離れた回答は、次の障害物が見え始めたか、外れた回答として扱います
  • box の左端のずれを足す。 自機から障害物までの距離が 50 px 以上のとき、box の左端は平均 6〜7 px 左にずれていました。その分、6 px 足しています

4-3. 実際に動かしてみた結果

 実際に動かしてみた結果が以下のgifになります。画像の送信間隔は200ms、速度レベルはいきなり 2 (320 px/秒)で試していますが、ずっと失敗せずに障害物を通過できています! (gif画像では、21秒生存して、25この障害物を通過しています)

3.gif

 Jevの判断やラグの補正がどのように働いているかをオーバレイで可視化しています。

  • 黒い枠:Jev が検出した障害物の位置。上の「柱 0.xx」は障害物の種類と confidenceの値を表示しています
  • 点線の枠:ゲーム側の処理でラグを補正した場合の、障害物の今の想定位置
  • 縦線「踏み切り」:障害物の左端がここに来たらジャンプする
  • 「推定距離 ○ px、ジャンプ予約 ○ ms」:ゲーム側で計算した今の障害物までの距離と、ジャンプするまでの時間

 黒い枠が実際の障害物より右側にずれて描画されていますが、これが実際に生じているラグになります。そのラグを前述の処理で補正した場合を示す点線の枠はちゃんと実際の障害物に重なっているため、うまくラグを補正できていることがわかります。

4-4. 速度レベルを上げてみるとどうなるか

 先ほどの結果は速度レベル 2(320 px/秒)でしたので、さらにゲーム速度を上げるとどうなるか試してみました。3 コースを 3 回ずつ計9回プレイして、それぞれ 障害物を 10 個越えたら打ち切りにしています。

速度レベル 障害物を 10 個越えた回数 越えた個数の中央値
1〜2.5 すべて 10 本
3 9 回中 6 回 10 本
3.5 9 回中 0 回 2 本
4 9 回中 0 回 1 本

速度レベル 2.5 (400 px/秒)までは安定していそうですが、3 を超えると早い段階でゲームオーバーになっています。

 速度レベル 3.5(560 px/秒)で動かしてみた際の実際のゲーム画面が以下になります。8つ目の障害物を越えようとして障害物にぶつかっています。ログを確認する限りでは、Jevからの回答がブラウザに届いたタイミングで、ゲーム側が見積もった自機と障害物の距離は約 13 px でした。つまり、ジャンプの適正タイミングを過ぎていまい、ジャンプが間に合っていないということになります。

4.gif

 これは、Jevへの画面情報の送信間隔が200msであることが原因だと考えられるので、送信間隔100msで試してみました。先ほどと同じ測定方法で確認したところ、結果は以下のようになりました。

速度レベル 送信間隔 200 ms ごと 送信間隔 100 ms ごと
3 倍 9 回中 6 回(中央値 10 本) 9 回中 9 回(10 本)
3.5 倍 9 回中 0 回(2 本) 9 回中 6 回(10 本)
4 倍 9 回中 0 回(1 本) 9 回中 3 回(5 本)

 速度レベルが3.5でもある程度対応できるようになりました!速度レベル 4(640 px/秒)だと流石にすぐにゲームオーバーになりますが、それでもいくつかの障害物は越えられていそうです。速度レベル3.5/送信間隔100msの場合の実際のゲーム画面は以下のgifになります。

5.gif

 ちなみに、ここまで色々検証のために Jev の API を叩いてきましたが、どれくらいのコストになったかというと...

スクリーンショット 2026-10-28 22.12.37.png

なんと $1もかかっていません! やはりこの安さは圧倒的だなと改めて感じました...

まとめ

 Jevのv1.14.0で対応された画像入力を活用して、Jevにゲームをプレイしてもらおうと色々試してみました!単純にゲーム画面を渡すだけでは判断の精度が良くありませんでしたが、criteriaやboxをうまく利用することで判断の精度が上がることが確認できました。途中から Jev には障害物の位置と種類を判断する役割に切り替えましたが、ラグの対策をもう少ししっかり考えれば、完全に Jev にプレイさせる場合でもしっかり障害物を越えてくれるようになるのではと感じました。
 今回は横スクロールのゲームをテーマとしましたが、「画面を見て判断」をいうのを生かすのであれば、色々なタイプのゲームに応用できそうですので、色々と試してみたいと思います!本記事は以上になります。ご精読ありがとうございました!

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?