【MoveNet/第4回】

MoveNet

ついにMoveNetも第4回、折り返しです。

MoveNetでゲームを作ろう

前回の課題で手に旗を持たせることから察しているかと思いますが、今回は旗揚げゲームを制作していきます。

「赤あげて」「白あげて」「赤さげないで、白さげて」みたいな出題があるやつですね。

旗揚げゲームをしたよ! - 渋谷区・新宿区・中野区|療育|アイビージュニア
こんにちは。アイビージュニアです! 今回は放課後デイサービスの時間で行った、旗揚げゲームの様子をご紹介します。 ルールはとても簡単な旗揚げゲームですが...

上記の事例だと足もあげていますね、今回は手のみの旗揚げゲームですが、MoveNetなら足を組み込んだ旗揚げゲームも作れるかもしれないですね。

旗揚げゲームを作ろう

旗揚げゲームはMoveNetを使った挙手の判定だけでなく、点数の加算や出題システムなど様々な要素を組み込む必要があります。皆さんも旗揚げゲームに何が必要か想像してみましょう。

筆者も何が必要か、ルールはどうしようか分かっていないので、まずは図に書き起こしてみます。

ということで書き起こしました。今回は出題を10回繰り返して、3回ミスをしたらゲームオーバーという形を取っています。

オレンジが処理で、紫が条件分岐です。緑が「真」、赤が「偽」を表しています。図にすると意外とシンプルですね。何とかなりそうです。

出題システムを作ろう

旗揚げゲームの醍醐味といえば、旗を上げるか上げないかを瞬時に判別するところです。各色の上げ下げだけでは4種類しかないので、多くの場合ゲーム性を高めるために「○あげないで、◯さげて」のようなフェイントが入ります。出題されるパターンを表に書き起こしてみました。

{色}には赤か白が入ります。

{色}あげて
{色}さげて
{色}あげないで{色}あげて
{色}さげて
{色}あげない
{色}さげない
{色}さげないで{色}あげて
{色}さげて
{色}あげない
{色}さげない

色も考慮すると全部で36パターンあります。結構多いですね。この36パターンをランダムで出題することで旗揚げゲームが成り立ちます。

どのように実装するかは人にもよるところですが、今回は以下のように実装してみます。

今回はコピー/実行はしなくてよいです

const patterns = [
    ['{色}あげて', ''],
    ['{色}さげて', ''],

    ['{色}あげないで', '{色}あげて'],
    ['{色}あげないで', '{色}さげて'],
    ['{色}あげないで', '{色}あげない'],
    ['{色}あげないで', '{色}さげない'],

    ['{色}さげないで', '{色}あげて'],
    ['{色}さげないで', '{色}さげて'],
    ['{色}さげないで', '{色}あげない'],
    ['{色}さげないで', '{色}さげない'],
];


function randomPattern() {
    const colors = ['赤', '白']; // 色のリスト
    const chosenColors = colors.map(color => color); // 色のコピーを作成

    const chosenPattern = patterns[Math.floor(Math.random() * patterns.length)]; // ランダムにパターンを選択

    // パターン内の色をランダムに置換して表示
    const result = chosenPattern.map(pattern => pattern.replace('{色}', chosenColors[Math.floor(Math.random() * chosenColors.length)]));
    return result;
}


for (let i = 1; i <= 20; i++) {
    console.log(i + ':' + randomPattern().join(' ')); // 結果をコンソールに表示
}

配列に先ほどの旗のパターンがありますね。ランダムにパターンを選んで、選んだ配列の{色}を赤か白にランダムに置き換えることで実現しています。

各行で様々な処理をしているので、じっくり確認してみてください。

Node.js(JavaScrip実行環境/今回はスルーしてください)で実行するとこんな感じになります。

上手くいきました!

正誤判定システムを作ろう

出題自体はうまくいきましたが、問題は正誤判定です。旗の状態管理までしないといけなくなりました。

まずはルールの確認です。それぞれの命令で、旗をどうしないといけないかを見てみましょう。

命令動作
{色}あげて{色}をあげる
{色}さげて{色}をさげる
{色}あげない{色}をさげる
{色}さげない{色}をあげる

こんな感じですね。例を出してみましょう。

命令赤(右手)白(左手)
白あげないで 赤さげないあげているさげている
赤さげてさげている前の状態を維持
赤あげないで 赤あげてあげている前の状態を維持
白さげないで 白あげない前の状態を維持さげている

これを見ると命令によってかなり結果がバラバラになります。出題だけなら簡単ですが、正誤判定となると結構大変です。

このあとコードが出てきますが、まずは自分でどのようにすればこの正誤判定が作れるか考えてみましょう。

こちらもコピー/実行はしなくてよいです
※ただしこの後の課題でこのコードを使用します

const patterns = [
    ['{色}あげて', ''],
    ['{色}さげて', ''],

    ['{色}あげないで', '{色}あげて'],
    ['{色}あげないで', '{色}さげて'],
    ['{色}あげないで', '{色}あげない'],
    ['{色}あげないで', '{色}さげない'],

    ['{色}さげないで', '{色}あげて'],
    ['{色}さげないで', '{色}さげて'],
    ['{色}さげないで', '{色}あげない'],
    ['{色}さげないで', '{色}さげない'],
];

var redState = 'さがっている'; // 赤旗の初期状態
var whiteState = 'さがっている'; // 白旗の初期状態

const state = [
    ['あげて', 'あがっている'],
    ['さげて', 'さがっている'],
    ['あげない', 'さがっている'],
    ['さげない', 'あがっている'],
];

function randomPattern() {
    const colors = ['赤', '白']; // 色のリスト
    const chosenColors = colors.map(color => color); // 色のコピーを作成

    const chosenPattern = patterns[Math.floor(Math.random() * patterns.length)]; // ランダムにパターンを選択

    // パターン内の色をランダムに置換して表示
    const result = chosenPattern.map(pattern => pattern.replace('{色}', chosenColors[Math.floor(Math.random() * chosenColors.length)]));
    return result;
}

function isFlagRised() {
    // ランダムな指示パターンを取得する
    const [instruction_1, instruction_2] = randomPattern();

    // 指示から状態を更新する関数
    const updateState = (instruction) => {
        // 指示が存在しない場合は何もしない
        if (!instruction) return;

        // 指示から色と名前を抽出
        const result = instruction.match(/(赤|白)(.+)/);
        const isRed = result[1].includes('赤'); // 赤か白かを判断
        const stateObj = state.find(([name]) => result[2].includes(name)); // 名前に一致する状態を探す
        const stateValue = stateObj ? stateObj[1] : null; // 状態の値を取得 (存在しない場合はnull)

        // 状態を更新
        if (isRed) {
            redState = stateValue; // 赤の場合は赤の状態を更新
        } else {
            whiteState = stateValue; // 白の場合は白の状態を更新
        }
    }

    // 2つの指示から状態を更新
    updateState(instruction_1);
    updateState(instruction_2);

    // 指示と最終的な状態を返す
    return `${instruction_1} ${instruction_2} (赤は${redState}、白は${whiteState})`;
}


for (let i = 1; i <= 20; i++) {
    console.log(i + ':' + isFlagRised()); // 結果をコンソールに表示
}

さて、コードの方ですが、isFlagRised()が追記されました。

instruction_1instruction_2には「赤あげて」のようなテキストが格納されます。このテキストから赤か白かを判別し、その後配列stateから当てはまる命令に対しての動作を返しています。

実行結果はこのようになります。

何が何だかさっぱりかと思うので、理解を促す課題を出したいと思います。

課題

今回は多用されていた関数に関する問題を解いたあとに、今回のサンプルコードに関する問題を出します。

準備として、まずは以下のHTMLファイルを任意のファイル名で保存してください。

<!DOCTYPE html>
<html lang="ja">
<head>
    <script src="./test.js"></script>
</head>
</html>

その後./test.jsをctrl+クリックします。ダイアログが出るので、「ファイルの作成」をクリックします。

これでtest.jsの作成も完了します。VSCodeの画面右下「Go Live」をクリックし、http://127.0.0.1:5500/{任意のファイル名}.htmlで開きます。

真っ白な画面が出ますが、気にせずF12キーを押して開発者モードに移行してください。

これで準備は完了です。

mapメソッドに関して

mapメソッドは配列の要素を加工して新しい配列を作成するのに便利です。

const fruits = ['apple', 'banana', 'orange', 'kiwi'];
const upperCaseFruits = fruits.map(fruit => fruit.toUpperCase());
console.log(upperCaseFruits);
  • 実行結果はどのようになるか、コードを実行する前に答えよ
['APPLE', 'BANANA', 'ORANGE', 'KIWI']

文字列を大文字にするtoUpperCase()メソッドも使われていますね。

  • 以下の配列を受け取り、mapメソッドを使って各数値を2倍にした新しい配列を作成せよ
const numbers = [1, 2, 3, 4, 5];
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(number => number * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]

replaceメソッドに関して

文字列置換に使用される重要なメソッドです。

const str = 'HELLO WORLD';
const newStr = str.replace(/[A-Z]/g, (match) => match.toLowerCase());
console.log(newStr);
  • 実行結果はどのようになるか、コードを実行する前に答えよ
hello world

文字列を小文字にするtoLowerCase()メソッドも使われていますね。

  • 次の文字列のハイフン(-)をスペース( )に置換せよ
const str = 'Hello-World-How-Are-You';
const str = 'Hello-World-How-Are-You';
const newStr = str.replace(/-/g, ' ');
console.log(newStr);

ここで大事なのが/gフラグです。これがあることで、すべての一致項目を置換できます。

includesメソッドに関して

文字列または配列内に指定の値が含まれているかどうかを判定するのに便利なメソッドです。

const str = 'ここで履物を脱いでください';
const includesHello = str.includes('着物');
console.log(includesHello);
  • 実行結果は真か偽か

偽(false)

すべてひらがなの場合は真になります。日本語の文字列は扱いが難しいですね。

  • 次の配列に{ name: ‘ザイン’, race: ‘human’ }が含まれているかどうかを確認せよ
const Frieren_group = [
    { name: 'フリーレン', race: 'elf' },
    { name: 'フェルン', race: 'human' },
    { name: 'シュタルク', race: 'human' },
];
const Frieren_group = [
    { name: 'フリーレン', race: 'elf' },
    { name: 'フェルン', race: 'human' },
    { name: 'シュタルク', race: 'human' },
];
const includesSein = Frieren_group.includes({ name: 'ザイン', race: 'human' });
console.log(includesSein);

実行結果は偽です。どうやらザインは離脱済みのようですね。

関数とメソッドの違いに関して

どちらもやれることが近しいですが、意味が違うので注意。ただコードを書いていて気にする瞬間はあまりないです。

「関数」と「メソッド」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
「関数」と「メソッド」の違いを何となく説明しています。

コード書き換え問題

次回に向けていくつかコードの修正をお願いします。

まずはこのコードをtest.jsにコピーします。コピーができたら下記を解いてみましょう。

  • 「あがっている」はtrue、「さがっている」はfalseと表示されるようにせよ

実行結果例はこんな感じです。

問題の該当部分もいくつか修正しています。

const patterns = [
    ['{色}あげて', ''],
    ['{色}さげて', ''],

    ['{色}あげないで', '{色}あげて'],
    ['{色}あげないで', '{色}さげて'],
    ['{色}あげないで', '{色}あげない'],
    ['{色}あげないで', '{色}さげない'],

    ['{色}さげないで', '{色}あげて'],
    ['{色}さげないで', '{色}さげて'],
    ['{色}さげないで', '{色}あげない'],
    ['{色}さげないで', '{色}さげない'],
];

var redState = false; // 赤旗の初期状態
var whiteState = false; // 白旗の初期状態

const state = [
    ['あげて', true],
    ['さげて', false],
    ['あげない', false],
    ['さげない', true],
];

function randomPattern() {
    const colors = ['赤', '白']; // 色のリスト
    const chosenColors = colors.map(color => color); // 色のコピーを作成

    const chosenPattern = patterns[Math.floor(Math.random() * patterns.length)]; // ランダムにパターンを選択

    // パターン内の色をランダムに置換して表示
    const result = chosenPattern.map(pattern => pattern.replace('{色}', chosenColors[Math.floor(Math.random() * chosenColors.length)]));
    return result;
}

function isFlagRised() {
    // ランダムな指示パターンを取得する
    const [instruction_1, instruction_2] = randomPattern();

    // 指示から状態を更新する関数
    const updateState = (instruction) => {
        // 指示が存在しない場合は何もしない
        if (!instruction) return;

        // 指示から色と名前を抽出
        const result = instruction.match(/(赤|白)(.+)/);
        const isRed = result[1].includes('赤'); // 赤か白かを判断
        const stateObj = state.find(([name]) => result[2].includes(name)); // 名前に一致する状態を探す
        const stateValue = stateObj ? stateObj[1] : null; // 状態の値を取得 (存在しない場合はnull)

        // 状態を更新
        if (isRed) {
            redState = stateValue; // 赤の場合は赤の状態を更新
        } else {
            whiteState = stateValue; // 白の場合は白の状態を更新
        }
    }

    // 2つの指示から状態を更新
    updateState(instruction_1);
    updateState(instruction_2);

    // 指示と最終的な状態を返す
    return `${instruction_1} ${instruction_2} ${redState} ${whiteState}`;
}


for (let i = 1; i <= 20; i++) {
    console.log(isFlagRised()); // 結果をコンソールに表示
}

まとめ

お疲れ様でした。ゲームを作ろうとなると、処理の順番やアルゴリズムを考えないといけないので、プログラミングの能力だけでなく、論理的思考力も必要になってきます。

ただ世の中のゲームで評価されるのは中身のロジックではなく、見た目なんですよね。悲しいですが。

次回はMoveNetで手を挙げたかどうか、などの条件分岐処理をやっていきます。

それでは。