Skip to content

Commit

Permalink
WIP: GameSessionController2
Browse files Browse the repository at this point in the history
  • Loading branch information
Konrad Jamrozik committed Jun 7, 2024
1 parent 97cac79 commit dc1a5fe
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/game-lib/Controller/GameSessionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void PlayGameSession(int turnLimit, IPlayer player)
List<WorldEvent> worldEvents = GetAndDeleteRecordedWorldEvents();
GameSession.CurrentGameEvents.AddRange(worldEvents);

// This state diff shows the result of the action the player took in their turn.
// This state diff shows the result of advancing time.
DiffPreviousAndCurrentGameState();
}

Expand Down
185 changes: 185 additions & 0 deletions src/game-lib/Controller/GameSessionController2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using Lib.Contracts;
using UfoGameLib.Events;
using UfoGameLib.Lib;
using UfoGameLib.Model;
using UfoGameLib.Reports;
using UfoGameLib.State;

namespace UfoGameLib.Controller;

/// <summary>
/// Represents means for controlling GameSession, to be called by client logic (e.g. CLI) acting on behalf of
/// a player, whether human or automated.
///
/// Provides following features, as compared to accessing GameSession directly:
/// - Convenient methods representing player actions that are translated by the controller
/// to underlying low-level GameSession methods invocations.
/// - Restricted Read/Write access to the GameSession. Notably, a player should not be able
/// to read entire game session state, only parts visible to them.
///
/// Here are few scenarios of using GameSessionController:
///
/// 1. A human player calls the CLI executable built from game-cli.
/// The implementation of that executable, Program.cs, translates the CLI commands to invocations
/// of GameSessionController methods. The output of these methods is returned through Program.cs to the human player.
/// 2. As 1. but the CLI commands are called not by a human, but by an automated process (aka automated player).
/// 3. The CLI executable is used by human player to launch a game session using an AI player.
/// As a result, the CLI program ends up instantiating AIPlayer instance which then plays through the game by
/// invoking methods on GameSessionController.
/// 4. As 3. but the CLI commands are called not by a human, but by an automated process (aka automated player).
///
/// The scenarios above can be visualized as follows, where "--" should be read as:
/// "Left side invokes right side, and right side returns output to the left side".
///
/// ```
/// 1. Human player -- CLI executable -- Program -- GameSessionController -- GameSession
/// 2. Automated player -- CLI executable -- Program -- GameSessionController -- GameSession
/// 3. Human player -- CLI executable -- Program -- AIPlayer -- GameSessionController -- GameSession
/// 4. Automated player -- CLI executable -- Program -- AIPlayer -- GameSessionController -- GameSession
/// ```
/// </summary>
public class GameSessionController2
{
public readonly GameTurnController TurnController;

protected readonly GameSession2 GameSession;
private readonly Configuration _config;
private readonly ILog _log;

public GameSessionController2(Configuration config, ILog log, GameSession2 gameSession)
{
_config = config;
_log = log;
GameSession = gameSession;
TurnController = new GameTurnController(_log, GameSession.RandomGen, GameSession.CurrentGameState);
}

public GameStatePlayerView CurrentGameStatePlayerView
=> new GameStatePlayerView(() => GameSession.CurrentGameState);

public void PlayGameSession(int turnLimit, IPlayer player)
{
// Assert:
// IF the GameSession was ctored with null initialGameState,
// THEN CurrentGameStatePlayerView.CurrentTurn == Timeline.InitialTurn
Contract.Assert(CurrentGameStatePlayerView.CurrentTurn >= Timeline.InitialTurn);
Contract.Assert(turnLimit <= GameState.MaxTurnLimit);
Contract.Assert(turnLimit >= CurrentGameStatePlayerView.CurrentTurn);


PlayGameUntilOver(player, turnLimit);

var endState = GameSession.CurrentGameState;

_log.Info("");
_log.Info(
$"===== Game over! " +
$"Game result: {(endState.IsGameLost ? "lost" : endState.IsGameWon ? "won" : "undecided")}");
_log.Info($"Money: {endState.Assets.Money}, " +
$"Intel: {endState.Assets.Intel}, " +
$"Funding: {endState.Assets.Funding}, " +
$"Upkeep: {endState.Assets.Agents.UpkeepCost}, " +
$"Support: {endState.Assets.Support}, " +
$"Transport cap.: {endState.Assets.MaxTransportCapacity}, " +
$"Missions launched: {endState.Missions.Count}, " +
$"Missions successful: {endState.Missions.Successful.Count}, " +
$"Missions failed: {endState.Missions.Failed.Count}, " +
$"Mission sites expired: {endState.MissionSites.Expired.Count}, " +
$"Agents: {endState.Assets.Agents.Count}, " +
$"Terminated agents: {endState.TerminatedAgents.Count}, " +
$"Turn: {endState.Timeline.CurrentTurn} / {turnLimit}.");

SaveCurrentGameStateToFile();

new GameSessionStatsReport2(
_log,
GameSession,
_config.TurnReportCsvFile,
_config.AgentReportCsvFile,
_config.MissionSiteReportCsvFile,
endState.Timeline.CurrentTurn)
.Write();

_log.Flush();
}

private void PlayGameUntilOver(IPlayer player, int turnLimit)
{
// Note: in the boundary case of
//
// turnLimit == GameSession.CurrentGameState.Timeline.CurrentTurn
//
// e.g. when the game session is new and turnLimit == Timeline.InitialTurn,
// the game session will be immediately over, without the player getting a chance to do
// anything.
while (!GameSessionOver(GameSession.CurrentGameState, turnLimit))
{
_log.Info("");
_log.Info($"===== Turn {GameSession.CurrentGameState.Timeline.CurrentTurn}");
_log.Info("");

player.PlayGameTurn(CurrentGameStatePlayerView, TurnController);

List<PlayerActionEvent> playerActionEvents = TurnController.GetAndDeleteRecordedPlayerActionEvents();
GameSession.CurrentGameEvents.AddRange(playerActionEvents);

if (GameSession.CurrentGameState.IsGameOver)
break;

Contract.Assert(!GameSession.CurrentGameState.IsGameOver);

// This state diff shows what actions the player took.
DiffGameStates(GameSession.CurrentTurn.StartState, GameSession.CurrentGameState);

GameState nextTurnStartState = GameSession.CurrentGameState.Clone();

PlayerActionEvent advanceTimePlayerActionEvent = AdvanceTime(nextTurnStartState);
List<WorldEvent> worldEvents = GetAndDeleteRecordedWorldEvents();

// This state diff shows the result of advancing time.
DiffGameStates(GameSession.CurrentGameState, nextTurnStartState);

GameSession.Turns.Add(new GameSessionTurn2(
eventsUntilStartState: [advanceTimePlayerActionEvent, ..worldEvents],
startState: nextTurnStartState));
}
}

private List<WorldEvent> GetAndDeleteRecordedWorldEvents()
{
// kja to implement GetAndDeleteRecordedWorldEvents
return new List<WorldEvent>();
}

public PlayerActionEvent AdvanceTime(GameState state)
=> new AdvanceTimePlayerAction(_log, GameSession.RandomGen).Apply(state);

public GameState SaveCurrentGameStateToFile()
{
GameSession.CurrentGameState.ToJsonFile(_config.SaveFile);
_log.Info($"Saved game state to {_config.SaveFile.FullPath}");
return GameSession.CurrentGameState;
}

public GameState LoadCurrentGameStateFromFile()
{
GameState loadedGameState = GameState.FromJsonFile(_config.SaveFile);
GameSession.Turns.Add(new GameSessionTurn2(startState: loadedGameState));
_log.Info($"Loaded game state from {_config.SaveFile.FullPath}");
return GameSession.CurrentGameState;
}

private static bool GameSessionOver(GameState state, int turnLimit)
{
Contract.Assert(
state.Timeline.CurrentTurn <= turnLimit,
"It should not be possible for current state turn to go above turnLimit");
return state.IsGameOver || state.Timeline.CurrentTurn == turnLimit;
}

private void DiffGameStates(GameState prev, GameState curr)
{
Contract.Assert(!ReferenceEquals(prev, curr));
new GameStateDiff(prev, curr).PrintTo(_log);
}
}
20 changes: 11 additions & 9 deletions src/game-lib/Controller/GameTurnController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,28 @@ public void LaunchMission(MissionSite site, int agentCount)
// Then the basic AI needs to be updated to handle this.

public PlayerActionEvent HireAgents(int count)
=> RecordPlayerActionEvent(new HireAgentsPlayerAction(_log, count));
=> ExecuteAndRecordAction(new HireAgentsPlayerAction(_log, count));

public PlayerActionEvent BuyTransportCapacity(int capacity)
=> RecordPlayerActionEvent(new BuyTransportCapacityPlayerAction(_log, capacity));
=> ExecuteAndRecordAction(new BuyTransportCapacityPlayerAction(_log, capacity));

public PlayerActionEvent SackAgents(Agents agents)
=> RecordPlayerActionEvent(new SackAgentsPlayerAction(_log, agents));
=> ExecuteAndRecordAction(new SackAgentsPlayerAction(_log, agents));

public PlayerActionEvent SendAgentsToTraining(Agents agents)
=> RecordPlayerActionEvent(new SendAgentsToTrainingPlayerAction(_log, agents));
=> ExecuteAndRecordAction(new SendAgentsToTrainingPlayerAction(_log, agents));

public PlayerActionEvent SendAgentsToGenerateIncome(Agents agents)
=> RecordPlayerActionEvent(new SendAgentsToGenerateIncomePlayerAction(_log, agents));
=> ExecuteAndRecordAction(new SendAgentsToGenerateIncomePlayerAction(_log, agents));

public PlayerActionEvent SendAgentsToGatherIntel(Agents agents)
=> RecordPlayerActionEvent(new SendAgentsToGatherIntelPlayerAction(_log, agents));
=> ExecuteAndRecordAction(new SendAgentsToGatherIntelPlayerAction(_log, agents));

public PlayerActionEvent RecallAgents(Agents agents)
=> RecordPlayerActionEvent(new RecallAgentsPlayerAction(_log, agents));
=> ExecuteAndRecordAction(new RecallAgentsPlayerAction(_log, agents));

public PlayerActionEvent LaunchMission(MissionSite site, Agents agents)
=> RecordPlayerActionEvent(new LaunchMissionPlayerAction(_log, site, agents));
=> ExecuteAndRecordAction(new LaunchMissionPlayerAction(_log, site, agents));

public List<PlayerActionEvent> GetAndDeleteRecordedPlayerActionEvents()
{
Expand All @@ -93,8 +93,10 @@ private MissionSite GetMissionSiteById(int siteId) =>
private Agents GetAgentsByIds(int[] agentsIds) =>
_gameState.Assets.Agents.GetByIds(agentsIds);

private PlayerActionEvent RecordPlayerActionEvent(PlayerAction action)
private PlayerActionEvent ExecuteAndRecordAction(PlayerAction action)
{
// This assertion is here to prevent the player of doing anything if they caused the game to be over.
Contract.Assert(!_gameState.IsGameOver);
PlayerActionEvent playerActionEvent = action.Apply(_gameState);
_recordedPlayerActionEvents.Add(playerActionEvent);
return playerActionEvent;
Expand Down
41 changes: 41 additions & 0 deletions src/game-lib/Reports/GameSessionStatsReport2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using UfoGameLib.Lib;
using UfoGameLib.State;
using File = Lib.OS.File;

namespace UfoGameLib.Reports;

public class GameSessionStatsReport2
{
private readonly ILog _log;
private readonly GameSession2 _gameSession;
private readonly File _turnReportCsvFile;
private readonly File _agentReportCsvFile;
private readonly File _missionSiteReportCsvFile;
private readonly int _lastTurn;


public GameSessionStatsReport2(
ILog log,
GameSession2 gameSession,
File turnReportCsvFile,
File agentReportCsvFile,
File missionSiteReportCsvFile,
int lastTurn)
{
_log = log;
_gameSession = gameSession;
_turnReportCsvFile = turnReportCsvFile;
_agentReportCsvFile = agentReportCsvFile;
_missionSiteReportCsvFile = missionSiteReportCsvFile;
_lastTurn = lastTurn;
}

public void Write()
{
List<GameState> gameStates = _gameSession.GameStates.ToList();

new TurnStatsReport(_log, gameStates, _turnReportCsvFile).Write();
new AgentStatsReport(_log, _gameSession.CurrentGameState, _agentReportCsvFile, _lastTurn).Write();
new MissionSiteStatsReport(_log, _gameSession.CurrentGameState, _missionSiteReportCsvFile).Write();
}
}
40 changes: 40 additions & 0 deletions src/game-lib/State/GameSession2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Lib.Primitives;
using MoreLinq;
using UfoGameLib.Events;
using UfoGameLib.Lib;

namespace UfoGameLib.State;

/// <summary>
/// GameSession represents an instance of a game session (a playthrough).
///
/// As such, it maintains a reference to current GameState.
///
/// In addition, it allows updating of the game state by applying PlayerActions.
///
/// GameSession must be accessed directly only by GameSessionController.
/// </summary>
public class GameSession2
{
public readonly RandomGen RandomGen;

public List<GameSessionTurn2> Turns;


public GameSessionTurn2 CurrentTurn => Turns.Last();

public GameState CurrentGameState => CurrentTurn.EndState;

public List<GameEvent> CurrentGameEvents => CurrentTurn.EventsInTurn;

public GameSession2(RandomGen randomGen, List<GameSessionTurn2>? turns = null)
{
RandomGen = randomGen;
Turns = turns ?? [new GameSessionTurn2()];
}

public IReadOnlyList<GameState> GameStates
=> Turns.SelectMany<GameSessionTurn2, GameState>(turn => [turn.StartState, turn.EndState])
.ToList()
.AsReadOnly();
}

0 comments on commit dc1a5fe

Please sign in to comment.