Design migliorato per un gioco multiplayer a turni che utilizza l'intelligenza artificiale?

6

Sto cercando di decidere quale sia la migliore architettura per un gioco multiplayer a turni in cui i giocatori possono essere umani o AI e l'interfaccia utente è opzionale, ad esempio perché il gioco può essere utilizzato solo per far combattere le IA contro l'altro.

Prendiamo il gioco più semplice possibile, tic-tac-toe, e ho usato una classe come questa:

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

Nella più semplice implementazione del mio gioco potrei avere un'interfaccia utente con un gestore di clic:

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

Questo codice forse funziona bene quando ci sono solo giocatori umani, ma se avessimo giocatori di IA e giochi "senza testa" diventerebbe insufficiente.

Quali sono alcune idee per espandere questo codice per gli altri casi d'uso (AI, gioco senza testa)?

Una prima soluzione sarebbe usare il modello classico dell'osservatore. Usando questa idea, più giocatori si iscriverebbero al gioco e verrebbero avvisati quando sarà il loro turno. Allo stesso modo, l'interfaccia può essere registrata e notificata quando devono essere visualizzate nuove configurazioni diverse.

Quindi in quel caso la classe di gioco cambierebbe in questo modo:

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

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

dove gli osservatori sarebbero i giocatori e l'interfaccia utente:

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

ma questa soluzione sembra un po 'troppo generica e non sono del tutto sicuro del modo migliore per descrivere il fatto che le IA possono rappresentare (per esempio) il primo e il terzo giocatore in un gioco.

Una soluzione più avanzata sarebbe utilizzare il pattern di osservatore per l'interfaccia utente, ma mantenere un sistema dedicato per i giocatori:

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));
    }
}

Ma le cose cominciano a diventare più complesse, e non sono sicuro che la modellazione di un giocatore umano avrebbe molto senso ora.

Non ho mai scritto un gioco nella mia vita quindi non sono del tutto sicuro se ci sono forse schemi che dovrei sapere o se la soluzione è più dipendente dal contesto.

Quali sono le tue opinioni sul mio progetto iniziale?

Potrebbe anche essere importante aggiungere che il contesto in cui vorrei scrivere il gioco è il web e che il framework UI sarebbe React.

    
posta heapOverflow 05.12.2018 - 16:41
fonte

4 risposte

5

Vorrei provare a mantenere il TicTacToeGame completamente agnostico dell'interfaccia utente. Nessun osservatore, nessun sottoscrittore di editori all'interno di quella classe. Solo "business logic" (o chiamala "logica di gioco") all'interno di quella classe, nessuna responsabilità mista che potrebbe portare alla complessità che hai scartato nella tua domanda.

Invece, è possibile implementare la logica di rotazione utilizzando la propria coda di eventi. Do un esempio di pseudo-codice usando il polling per semplicità, a seconda del tuo ambiente puoi implementarlo senza eseguire il polling:

  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);
          }
      }
  }

E i gestori di eventi dell'interfaccia utente (guidati dal loop di eventi dell'interfaccia utente, non dai tuoi) quindi dovrebbero avere qualche logica per contrassegnare una cella da parte dell'utente e inserire anche una Event.MoveCompleted nella coda:

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

Ovviamente, l'uso di una coda è un po 'eccessivamente ingegnerizzato nell'esempio sopra, poiché attualmente esiste un solo tipo di evento, quindi una semplice booleana globale farebbe anche il trucco. Ma nel tuo sistema reale, presumo che ci saranno diversi tipi di eventi, quindi ho cercato di dare un quadro approssimativo su come potrebbe essere il sistema. Spero che ti venga l'idea.

    
risposta data 05.12.2018 - 17:28
fonte
3

Vorrei andare con il modello di strategia .

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;
}

In tal caso, attendere gli input del player è bloccare il ciclo, ai non lo è.

    
risposta data 05.12.2018 - 18:01
fonte
0

Potresti usare un iterabile e amp; invia valori (in Python).

Il codice utilizza varie funzionalità avanzate di Python come dataclass es , invio di valori a generatori iteratori e utilizzando deque s per consumare iteratori , ma potrebbe essere possibile tradurre in altre lingue.

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
    
risposta data 06.12.2018 - 04:01
fonte
0

Personalmente non avrei generalizzato e astratto la tua IA e il giocatore controllato dall'uomo insieme in termini di mosse. Vale a dire, non modellerei un'interfaccia in cui "Umano" e "IA" sono sottotipi diversi. La ragione principale per cui penso è perché non penso che semplifica molto ma inoltre impone alcuni vincoli imbarazzanti per aggirare:

  1. Può diventare imbarazzante fare cose come se l'IA suggerisse mosse per il giocatore o per l'IA di prendere il sopravvento se il giocatore umano si ferma o si abbandona e non fa una mossa per 10 secondi, per esempio
  2. Potrebbe imporre delle difficoltà nell'implementazione poiché implicherebbe che l'interfaccia utente lavori con funzioni di input bloccanti quando viene richiesto a un giocatore di eseguire una mossa, e questo potrebbe essere piuttosto imbarazzante se non addirittura non pratico se si utilizza una GUI di gestione degli eventi API.

Quindi suggerirei di non pensare a cose come "Giocatori" e "IA" come oggetti concettuali "all'interno" del campo di gioco o del tabellone. Sono fuori dai "controllori" del gioco. Ha abbastanza senso? La mia formulazione potrebbe essere un po 'strana.

Ma se lo fai nel modo che suggerisco, allora dovrebbe essere facile in ogni dato momento permettere che le mosse di un giocatore umano vengano prese in carico dall'IA semplicemente chiamando una funzione di tipi che valuta le unità / scacchiere di quel giocatore e fa una mossa per lui / lei, che tu voglia questa caratteristica o meno. Suggerisco questo anche se non hai bisogno di quella funzione perché in realtà penso che l'implementazione risultante sarebbe ancora più semplice nonostante questa maggiore flessibilità.

Come esempio più complesso, considera un gioco di strategia a turni. Potresti avere units sulla lavagna e forse con proprietà come punti ferita, punti magici, quanti danni fanno, ecc. Quelli che potresti modellare come oggetti sotto un'interfaccia astratta. E i gruppi di quelle unità potrebbero appartenere a regni particolari, ei regni si muovono a turno.

Ma ciò che controlla le mosse per un particolare regno è qualcosa che separerei completamente dalla logica del "campo di battaglia" ed è invece associato esternamente, come Kingdom 1 potrebbe attualmente essere controllato da clic del mouse sull'interfaccia utente (forse con l'AI che fa una mossa se nessuna mossa viene eseguita dopo dieci secondi), Kingdom 2 potrebbe essere controllata rigorosamente da AI, Kingdom 3 potrebbe essere controllata da messaggi socket su una rete.

What are some ideas to expand this code for the other use cases (AI, headless game)?

Personalmente non vedo questo come un problema particolarmente interessante da raggiungere per modelli di progettazione come l'osservatore. Si attraversano i turni. Se si prevede che una determinata svolta sia controllata dall'input dell'utente, non fare nulla finché non viene fornito l'input dell'utente (es: onClick event). Dopo aver effettuato una mossa, vai alla prossima svolta. Se il prossimo turno "Player" o "Kingdom" o "Team" o qualunque cosa è contrassegnato come controllato dall'IA, quindi basta chiamare la funzione AI per valutare il campo di battaglia / board e fare una mossa e andare alla mossa successiva, e ripetere fino a quando il gioco è finito.

[...] I'm not entirely sure about the best way to describe the fact that the AIs may represent (for instance) the first and third players in a game.

Potrebbe essere semplice come una variabile booleana, come ai=true . Quando avanzi un turno, controlla lo stato del booleano associato a Player N . Se ai is true , chiama semplicemente una funzione per consentire all'IA di effettuare una mossa per quel giocatore e poi di tornare indietro. Se non è impostato su true, non fare nulla finché l'utente non fornisce input (es: clic su qualcosa).

L'astrazione della nozione di come un "Controller" qui sembra imbarazzante con pochi o nessun vantaggio per l'estensione rispetto a solo una proprietà che l'interfaccia utente può interrogare, ad esempio, perché ancora una volta si inizia a lavorare per bloccare le funzioni di input se così può diventare ingombrante con un sacco di API GUI (anche se ti capita di essere fantasioso e vuoi farlo attraverso una rete, il blocco fino al ricevimento di un messaggio socket è anche piuttosto scomodo). Quindi è più semplice trasformare questo in uno stato che puoi interrogare. Questo giocatore è controllato da un essere umano usando la GUI o l'intelligenza artificiale o qualcos'altro? È qualcosa da interrogare come lo vedo in ogni turno per decidere cosa fare esternamente piuttosto che invertire il flusso.

Per i giochi senza testa controllati rigorosamente dall'IA, potresti volere deliberatamente rallentare la velocità con cui l'IA fa le mosse. In quel caso puoi semplicemente disabilitare i controlli della GUI del giocatore se un turno avanza e ai is true per quel giocatore (abilitandoli di nuovo se il turno passa ad un giocatore controllato dall'uomo), per così dire, e avere, per esempio, un evento del timer in realtà chiama la funzione per far muovere l'IA. Sta entrando in un territorio sfumato ma lo stesso approccio generale.

    
risposta data 06.12.2018 - 08:12
fonte