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.