I'm trying to decide which is the best architecture for a turn-based multiplayer game where the players can be either humans or AI and the UI is optional, for instance because the game can just be used to make the AIs fight against each other.

Let's take the simplest possible game out there, tic-tac-toe, and I used a class like this:

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

In the most simple implementation of my game I may have a UI with a click handler:

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

This code maybe works fine when there are only human players but if we had AI players and "headless" games it becomes insufficient.

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

A first solution would be to use the classical observer pattern. By using this idea, multiple players would subscribe to the game and would be notified when it's their turn. In the same way, the interface could subscribe and be notified when new different configurations need to be displayed.

So in that case the game class would change to become like this:

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

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

where the observers would be the players and the UI:

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

but this solution looks a little bit too generic and 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.

A more advanced solution would be to use the observer pattern for the UI but keep a dedicated system for the players:

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

But things start to get more complex, and I'm not sure if modelling a human player would make that much sense now.

I've never written a game in my life so I'm not entirely sure if there are maybe patterns that I should know or if the solution is more context dependent.

What are your opinions about my initial design?

It may be also important to add that the context where I would like to write the game is the web, and the UI framework would be React.

  • Are you decoupling UI and controls e.g. AI can access controls directly? – Victor S Dec 5 at 15:53
  • 2
    Well written question, clearly a conceptional one (which makes it very on-topic for this site), but anyway you got a downvote with no explaining comment. Sometimes I feel very ashamed for this extremely unprofessional behaviour of this community (+1 from me). – Doc Brown Dec 5 at 15:55
  • 1
    @DocBrown: To be fair, as well-written as this question is, it lacks focus on a clear goal, and essentially it amounts to a "review my hypothetical designs" question (something we don't generally do here). "Proven" is not something we can demonstrate. – Robert Harvey Dec 5 at 16:08
  • @RobertHarvey: that's not an excuse for downvoting without a comment. – Doc Brown Dec 5 at 16:09
  • 2
    @DocBrown: Huh? Participants on Stack Exchange are not obliged to explain their downvotes (a practice that causes more problems than it solves). Downvoting is anonymous for a reason. – Robert Harvey Dec 5 at 16:10

I would try to keep the TicTacToeGame completely UI agnostic. No observer, no publisher-subscriber inside that class. Only "business logic" (or call it "game-logic") inside that class, no mixed responsibilities which could lead to the complexity you scetched in your question.

Instead, you could implement the turn-logic by utilizing your own event queue. I give an example in pseudo-code using polling for the sake of simplicity, depending on your environment you can implement it without polling instead:

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

And the event handlers of the UI (driven by the UI event loop, not yours) then should have some logic to mark a cell by the user and insert an Event.MoveCompleted into the queue as well:

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

Of course, using a queue is a little bit overengineered in the example above, since there is currently only one type of event, so a simple global boolean flag would do the trick as well. But in your real system, I assume there will be different types of events, so I tried to gave a rough outline on how the system may look like. I hope you get the idea.

  • Depending on implementation an interface could be another way of achieving this – Liath Dec 5 at 16:38
  • @Liath: ??? No idea what you have in mind. – Doc Brown Dec 5 at 16:40
  • This is an interesting answer because I can really see how my question was incomplete or too vague. More in details I'm trying to write this game in a web environment, where a loop is not really a viable solution. I can see how an event loop could fit instead. – heapOverflow Dec 5 at 16:48
  • @heapOverflow: I guess you can transfer my scetch also to a web application. It probably does not really matter if the incoming user actions are communicated by some desktop UI framework or through HTTP events, the difference may be just how you implement the queue.Wait() mechanism. – Doc Brown Dec 5 at 16:52
  • 1
    Having your question vague proves that answer even more, as it’s simply saying to implement business rules away from anything related to a UI. With this logic you could make your game in any language on any device and the code would be a lot similar every time. – Steve Chamaillard Dec 5 at 19:24

I would go with the strategy pattern.

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 that case waiting for player inputs is blocks the loop, ai is not.

  • I'm not really sure about this solution. I get the move by player one, I get the move by player two but I never use those? – heapOverflow Dec 5 at 17:09
  • I focused on the basics on my example, there are many way to implement it. I edit my answer. – Peter Dec 5 at 17:14
  • Side note (no offense): this is essentially the same solution as in my answer, just with some different implementation details. Concurrency is made explicit by using async/await instead of polling, "strategy pattern" used instead of an explicit conditional, and the boolean flag instead of the queue I mentioned in the last paragraph. – Doc Brown Dec 5 at 18:39
  • @DocBrown Yeah basically both said: "get the players input and process it in a loop until the game is ends" :) – Peter Dec 5 at 19:45

You could use a iterables & send values (in Python).

The code uses various advanced Python features such as dataclasses, sending values to generator-iterators, and using deques to consume iterators, but it might be possible to translate to other languages.

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

Personally I would not generalize and abstract your AI and human-controlled player together in terms of making moves. That is to say, I would not model an interface where "Human" and "AI" are different subtypes. The main reason I think that is because I don't think it simplifies much but moreover imposes some awkward constraints to get around:

  1. It can become awkward to do things like have the AI suggest moves for the player or for the AI to take over if the human player quits or dozes off and doesn't make a move for 10 seconds, e.g.
  2. It could impose difficulties in implementation since it would imply the UI works with blocking input functions when a player is requested to make a move, and that could be rather awkward if not outright impractical if you're using an event-handling GUI API.

So I would suggest not to think of things like "Players" and "AI" as conceptual objects "inside" your game field or board. They're outside "controllers" of the game. Does that make enough sense? My wording might be a bit weird.

But if you do this the way I suggest, then it should be easy at any given moment to allow a human player's moves be taken over by the AI by simply calling a function of sorts which evaluates that player's units/board and makes a move for him/her, whether you want this feature or not. I suggest this even if you don't need that feature because I actually think the resulting implementation would be even simpler in spite of this added flexibility.

As a more complex example, consider a turn-based strategy game. You might have units on the board and perhaps with properties like hit points, magic points, how much damage they do, etc. Those you might model as objects under some abstract interface. And groups of those units might belong to particular kingdoms, and the kingdoms make moves in turns.

But what controls the moves for a particular kingdom is something I'd separate completely from the logic of the "battlefield" and is instead externally associated, like Kingdom 1 might currently be controlled by mouse clicks to the UI (perhaps with the AI making a move if no move is performed after ten seconds), Kingdom 2 might be controlled strictly by AI, Kingdom 3 might be controlled by socket messages across a network.

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

Personally I don't see this as a particularly interesting problem to reach for design patterns like observer. You cycle through turns. If a particular turn is expected to be controlled by user input, don't do anything until user input is provided (ex: onClick event). After a move is made, you go to the next turn. If the next turn's "Player" or "Kingdom" or "Team" or whatever is marked as being AI-controlled, then just call the AI function to evaluate the battlefield/board and make a move and cycle to the next move, and repeat until the game is over.

[...] 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.

That could be as simple as a boolean variable, like ai=true. When you advance a turn, check the state of the boolean associated with Player N. If ai is true, then just call a function to let the AI make a move for that player and then advance turns again. If it's not set to true, then don't do anything until the user provides input (ex: clicks on something).

Abstracting the notion of like a "Controller" here seems awkward with little to no benefits for extension as opposed to just a property the UI can query, for example, because again you start working towards blocking input functions if so which can become unwieldy with lots of GUI APIs (also if you ever get fancy and want to do this across a network, blocking until a socket message is received is also rather awkward). So it's easier to just turn this into state you can query. Is this player being controlled by a human using the GUI or AI or something else? That's something to query as I see it on each turn to decide what to do externally rather than invert the flow.

For headless games controlled strictly by AI, you might want to deliberately slow down the rate at which the AI makes moves. In that case you can just disable the player GUI controls if a turn advances and ai is true for that player (enabling them again if the turn advances to a human-controlled player), so to speak, and have, say, a timer event actually call the function to cause the AI to make a move. That's getting into nuanced territory but same overall approach.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.