web-dev-qa-db-ja.com

AIを使用したターンベースのマルチプレイヤーゲームのデザインを改善しましたか?

私は、プレーヤーが人間またはAIのいずれかであり、UIがオプションであるターンベースのマルチプレーヤーゲームに最適なアーキテクチャを決定しようとしています。たとえば、ゲームを使用してAIを互いに戦わせることができます。 。

できる限り簡単なゲーム、tic-tac-toeを取り上げましょう。次のようなクラスを使用しました。

class TicTacToeGame {
    mark(cell) {
        //make something happen
    }
}

私のゲームの最も単純な実装では、クリックハンドラーを備えたUIがあるかもしれません。

function onClick(cell) {
    ticTacToeGame.mark(cell);
    refreshUI();
}

このコードは、人間のプレイヤーしかいない場合は問題なく機能する可能性がありますが、AIプレイヤーと「ヘッドレス」ゲームがある場合は不十分になります。

このコードを他のユースケース(AI、ヘッドレスゲーム)に拡張するためのアイデアは何ですか?

最初の解決策は、古典的なオブザーバーパターンを使用することです。このアイデアを使用することで、複数のプレイヤーがゲームをサブスクライブし、自分の番になると通知されます。同様に、インターフェースはサブスクライブし、新しい異なる構成を表示する必要があるときに通知を受けることができます。

その場合、ゲームクラスは次のように変更されます。

class TicTacToeGame {
    constructor() {
        this.observers = [];
    }
    subscribe(observer) {
        this.observers.Push(observer);
    }
    mark(cell) {
        //make something happen

        this.observers.forEach(o => o.notify(this));
    }
}

オブザーバーはプレイヤーとUIになります。

...
ticTacToeGame.register(AI);
ticTactoeGame.register(UI);
...

しかし、このソリューションは少し一般的すぎるように見え、AIが(たとえば)ゲームの最初と3番目のプレーヤーを表す可能性があるという事実を説明する最善の方法について完全にはわかりません。

より高度なソリューションは、UIにオブザーバーパターンを使用し、プレーヤー専用のシステムを維持することです。

class TicTacToeGame {
    constructor() {
        this.observers = [];
        this.players = [];
    }
    subscribe(observer) {
        this.observers.Push(observer);
    }
    addPlayer(player) {
        this.players.Push(player);
    }
    mark(cell) {
        //make something happen

        this.players[this.currentPlayerIndex].notify(this);
        this.observers.forEach(o => o.notify(this));
    }
}

しかし、状況はより複雑になり始めており、人間のプレーヤーをモデル化することが今ではそれほど意味があるかどうかはわかりません。

私は自分の人生でゲームを書いたことがないので、知っておくべきパターンがあるかどうか、またはソリューションがよりコンテキストに依存しているかどうかは完全にはわかりません。

私の最初のデザインについてどう思いますか?

また、私がゲームを書きたいコンテキストはWebであり、UIフレームワークはReactであることを追加することも重要です。

7
heapOverflow

私はTicTacToeGameを完全にUIにとらわれないようにしようと思います。そのクラス内にはオブザーバーもパブリッシャーサブスクライバーもいません。そのクラス内では「ビジネスロジック」(または「ゲームロジック」と呼ぶ)のみであり、質問の内容を複雑にする可能性のある混合責任はありません。

代わりに、独自のイベントキューを利用してターンロジックを実装できます。簡単にするために、ポーリングを使用した疑似コードの例を示しますが、環境によっては、ポーリングせずに実装できます。

  MainLoop()
  {
     while(queue.IsEmpty())
        WaitSomeMiliseconds(); // or use some queue.WaitForEvent() command, if available

     var nextEvent=queue.getNextEvent();
     if(nextEvent==Event.MoveCompleted)
     {
          Display(ticTacToeGame);
          if(ticTacToeGame.GameOver())
              break;
          nextPlayer=PickNextPlayer();
          if(nextPlayer.Type()==PlayerType.Human)
          {
             AllowMoveByUI();  // enable UI controls for entering moves by human
          }
          else
          { 
             LetAIMakeMove(ticTacToeGame);
             queue.Insert(Event.MoveCompleted);
          }
      }
  }

そして、UIのイベントハンドラー(ユーザーではなく、UIイベントループによって駆動される)には、ユーザーがセルをマークし、Event.MoveCompletedをキューに挿入するロジックも必要です。

  HandleUserInputEvent(CellType cell)
  {
      if(ticTacToeGame.IsMarkingValid(cell))
      {
         ticTacToeGame.Mark(cell);
         DisableMoveByUI();
         queue.Insert(Event.MoveCompleted);
      }
  }

もちろん、上記の例では、キューの使用は少しオーバーエンジニアリングされています。現在、イベントのタイプは1つしかないため、単純なグローバルブールフラグでも同じことができます。しかし、実際のシステムでは、さまざまなタイプのイベントがあると想定しているので、システムがどのように見えるかについて大まかな概要を説明しようとしました。私はあなたがアイデアを得ることを望みます。

5
Doc Brown

戦略パターン を使用します。

class Player {
    async getNextMove() {
        throw new Error('not implemented');
    };
}

class AiPlayer extends Player {
    async getNextMove() {
        /* Your AI LOGIC*/
        return 0;
    };
}

class HumanPlayer extends Player {
    async getNextMove() {
        await /*deal with user input*/
    };
}

// gameLogic:
let playerOne = new AiPlayer();
let playerTwo = new HumanPlayer();
let players = [playerOne, playerTwo];
let currentPlayer = 0;
let gameIsRuning =  true;
while (gameIsRuning) {
    let playerMove = await players[currentPlayer].getNextMove();
    // validate the input
    // recalculate the game state 
    // display board if not headless

    if (/*function to check game is over*/) {
        gameIsRuning = false;
    }
    currentPlayer = (currentPlayer++) % 2;
}

その場合、プレーヤーの入力を待つことがループをブロックしますが、aiはブロックしません。

3
Peter

イテラブル& send 値を使用できます(Pythonの場合)。

このコードは、さまざまな高度なPython dataclassesgenerator-iterators に値を送信し、 dequesを使用してイテレータを消費する が、それは他の言語に翻訳できるかもしれません。

from dataclasses import dataclass
from itertools import tee
from typing import Any, Callable, Generator, Iterable, MutableSequence, TypeVar

T = TypeVar('T')

# Utility classes

class Tee(Iterable[T]):  # Allows for an indefinite number of tees
    iterator: Iterable[T]
    previous: MutableSequence[T]

    def __init__(self, iterable: Iterable[T]):
        self.iterator = iter(iterable)
        self.previous = []

    def __iter__(self) -> '_TeeIterator[T]':
        return _TeeIterator(self)


class _TeeIterator(Iterator[T]):
    tee: Tee[T]
    i: int

    def __init__(self, tee: Tee[T]):
        self.tee = tee
        self.i = 0

    def __iter__(self) -> '_TeeIterator[T]':
        return self

    def __next__(self) -> T:
        try:
            return self.tee.previous[self.i]
        except IndexError:
            self.tee.previous.append(next(self.tee.iterable))
            return self.tee.previous[self.i]
        finally:
            self.i += 1

# Your code

@dataclass(frozen=True)
class Event:
    ...

@dataclass(frozen=True)
class Action:
    ...

def play_game(players: Callable[[], Generator[Action, Event, Any]]):  # Add additional parameters if necessary
    def game_iterable():
        activated_players = Tee(p() for p in players)
        activated_player_cycle = cycle(activated_players)

        deque(map(next, activated_players), 0)  # Allow each player to initialize itself. deque(..., 0) efficiently iterates through the given iterable

        def send_event(event: Event):
            for player in activated_players:
                player.send(event)


        for player in cycle(activated_players):
            move = next(player)
            # Process move and call send_event for each event

def example_player() -> Generator[Action, Event, None]:
    # Initialize

    while True:
        event = yield

        if event is None:
            pass  # yield actions
        else:
            pass  # Process the event
0
Solomon Ucko

個人的に、私はあなたのAIと人間が操作するプレイヤーを一緒に一般化して抽象化しません。つまり、「Human」と「AI」が異なるサブタイプであるインターフェースをモデル化しません。私が考える主な理由は、それほど単純化するとは思わないが、回避するために厄介な制約を課しているためです。

  1. AIがプレーヤーに動きを提案したり、人間のプレーヤーが終了または居眠りして10秒間動きをしなかった場合にAIが引き継いだりするなど、やりにくい場合があります。
  2. プレーヤーが移動を要求されたときにUIが入力機能をブロックして動作することを意味するため、実装が困難になる可能性があり、イベント処理GUI APIを使用している場合は、まったく実用的でないとしても、かなり扱いにくい可能性があります。

したがって、「プレーヤー」や「AI」のようなものを、ゲームフィールドやボードの「内部」にある概念的なオブジェクトとして考えないことをお勧めします。彼らはゲームの「コントローラー」の外にいます。それで十分でしょうか?私の言い回しは少し変かもしれません。

しかし、私が提案する方法でこれを行うと、人間のプレーヤーのユニット/ボードを評価して移動を行う種類の関数を呼び出すだけで、いつでも人間のプレーヤーの移動をAIに引き継ぐことが容易になりますこの機能が必要かどうかに関係なく、彼/彼女。この機能を必要としない場合でも、これをお勧めします。柔軟性が追加されたにもかかわらず、実際の実装はさらに単純になると思います。

より複雑な例として、ターンベースの戦略ゲームを考えてみましょう。ボード上にunitsがあり、ヒットポイント、マジックポイント、それらが与えるダメージの大きさなどのプロパティを持っている可能性があります。抽象的なインターフェイスの下でオブジェクトとしてモデル化する可​​能性のあるもの。そして、それらのユニットのグループは特定の王国に属している可能性があり、王国は順番に動きます。

しかし、特定の王国の動きを制御するのは、「バトルフィールド」のロジックから完全に分離するものであり、代わりに外部的に関連付けられます。たとえば、Kingdom 1は現在、UIへのマウスクリックによって制御されている可能性があります(おそらく10秒後に移動が行われない場合、AIは移動を行います)、Kingdom 2はAIによって厳密に制御される場合があり、Kingdom 3はネットワーク全体のソケットメッセージによって制御される場合があります。

このコードを他のユースケース(AI、ヘッドレスゲーム)に拡張するためのアイデアは何ですか?

個人的には、これをオブザーバーのようなデザインパターンに到達するための特に興味深い問題とは考えていません。あなたはターンを循環します。特定のターンがユーザー入力によって制御されることが予想される場合は、ユーザー入力が提供されるまで何もしないでください(例:onClickイベント)。移動後、次のターンに進みます。次のターンの「プレイヤー」、「キングダム」、「チーム」、またはAI制御されているものとしてマークされているものがある場合は、AI関数を呼び出して戦場/ボードを評価し、移動して次の移動にサイクルし、繰り返します。ゲームが終わるまで。

[...] AIが(たとえば)ゲームの最初と3番目のプレーヤーを表す可能性があるという事実を説明する最良の方法については、完全にはわかりません。

これは、ai=trueのように、ブール変数と同じくらい簡単です。ターンを進めるときは、Player Nに関連付けられているブール値の状態を確認してください。 ai is trueの場合は、関数を呼び出してAIにそのプレーヤーの動きをさせ、次にターンを進めます。 trueに設定されていない場合は、ユーザーが入力するまで何もしません(例:何かをクリックする)。

ここで「コントローラ」のような概念を抽象化すると、UIがクエリできるプロパティだけではなく、拡張のメリットがほとんどまたはまったくなく、たとえば、多くの場合扱いにくくなる可能性がある場合に入力関数のブロックに向けて作業を開始するので、不便に思われます。 GUI APIの使用(また、気が利いてネットワーク全体でこれを実行したい場合、ソケットメッセージが受信されるまでブロックすることもかなり厄介です)。したがって、これをクエリ可能な状態に変更する方が簡単です。このプレーヤーは、GUIまたはAIなどを使用して人間によって制御されていますか?フローを逆にするのではなく、外部で何をするかを決定するために、各ターンでそれを見ると、それはクエリするものです。

AIによって厳密に制御されるヘッドレスゲームの場合、AIが動く速度を故意に遅くしたい場合があります。その場合、ターンが進んだ場合はプレイヤーのGUIコントロールを無効にし、そのプレイヤーに対してai is trueを無効にする(ターンが人間が操作するプレイヤーに進んだ場合は再度有効にする)ことができます。タイマーイベントは実際に関数を呼び出して、AIを動かします。それは微妙な領域に入りつつありますが、全体的なアプローチは同じです。

0
Dragon Energy