私は、プレーヤーが人間または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であることを追加することも重要です。
私は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つしかないため、単純なグローバルブールフラグでも同じことができます。しかし、実際のシステムでは、さまざまなタイプのイベントがあると想定しているので、システムがどのように見えるかについて大まかな概要を説明しようとしました。私はあなたがアイデアを得ることを望みます。
戦略パターン を使用します。
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はブロックしません。
イテラブル& send 値を使用できます(Pythonの場合)。
このコードは、さまざまな高度なPython dataclass
es 、 generator-iterators に値を送信し、 deque
sを使用してイテレータを消費する が、それは他の言語に翻訳できるかもしれません。
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
個人的に、私はあなたのAIと人間が操作するプレイヤーを一緒に一般化して抽象化しません。つまり、「Human」と「AI」が異なるサブタイプであるインターフェースをモデル化しません。私が考える主な理由は、それほど単純化するとは思わないが、回避するために厄介な制約を課しているためです。
したがって、「プレーヤー」や「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を動かします。それは微妙な領域に入りつつありますが、全体的なアプローチは同じです。