-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Konrad Jamrozik
committed
Jun 7, 2024
1 parent
97cac79
commit dc1a5fe
Showing
5 changed files
with
278 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |