diff --git a/17 Bullfight/csharp/Game.csproj b/17 Bullfight/csharp/Game.csproj index 849a99d4..36c64642 100644 --- a/17 Bullfight/csharp/Game.csproj +++ b/17 Bullfight/csharp/Game.csproj @@ -1,4 +1,4 @@ - + Exe net5.0 diff --git a/17 Bullfight/csharp/src/BullFight.cs b/17 Bullfight/csharp/src/BullFight.cs new file mode 100644 index 00000000..8df6ff8c --- /dev/null +++ b/17 Bullfight/csharp/src/BullFight.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; + +namespace Game +{ + /// + /// Provides a method for simulating a bull fight. + /// + public static class BullFight + { + /// + /// Begins a new fight. + /// + /// + /// Object used to communicate with the player. + /// + /// + /// The sequence of events that take place during the fight. + /// + /// + /// After receiving each event, the caller must invoke the appropriate + /// mediator method to inform this coroutine what to do next. Failure + /// to do so will result in an exception. + /// + public static IEnumerable Begin(Mediator mediator) + { + var random = new Random(); + var result = ActionResult.FightContinues; + + var bullQuality = GetBullQuality(); + var toreadorePerformance = GetHelpQuality(bullQuality); + var picadorePerformance = GetHelpQuality(bullQuality); + + var bullStrength = 6 - (int)bullQuality; + var assistanceLevel = (12 - (int)toreadorePerformance - (int)picadorePerformance) * 0.1; + var bravery = 1.0; + var style = 1.0; + var passNumber = 0; + + yield return new Events.MatchStarted( + bullQuality, + toreadorePerformance, + picadorePerformance, + GetHumanCasualties(toreadorePerformance), + GetHumanCasualties(picadorePerformance), + GetHorseCasualties(picadorePerformance)); + + while (result == ActionResult.FightContinues) + { + yield return new Events.BullCharging(++passNumber); + + var (action, riskLevel) = mediator.GetInput<(Action, RiskLevel)>(); + result = action switch + { + Action.Dodge => TryDodge(riskLevel), + Action.Kill => TryKill(riskLevel), + _ => Panic() + }; + + var first = true; + while (result == ActionResult.BullGoresPlayer) + { + yield return new Events.PlayerGored(action == Action.Panic, first); + first = false; + + result = TrySurvive(); + if (result == ActionResult.FightContinues) + { + yield return new Events.PlayerSurvived(); + + var runFromRing = mediator.GetInput(); + if (runFromRing) + result = Flee(); + else + result = IgnoreInjury(action); + } + } + } + + yield return new Events.MatchCompleted( + result, + bravery == 2, + GetReward()); + + Quality GetBullQuality() => + (Quality)random.Next(1, 6); + + Quality GetHelpQuality(Quality bullQuality) => + ((3.0 / (int)bullQuality) * random.NextDouble()) switch + { + < 0.37 => Quality.Superb, + < 0.50 => Quality.Good, + < 0.63 => Quality.Fair, + < 0.87 => Quality.Poor, + _ => Quality.Awful + }; + + int GetHumanCasualties(Quality performance) => + performance switch + { + Quality.Poor => random.Next(0, 2), + Quality.Awful => random.Next(1, 3), + _ => 0 + }; + + int GetHorseCasualties(Quality performance) => + performance switch + { + // NOTE: The code for displaying a single horse casuality + // following a poor picadore peformance was unreachable + // in the original BASIC version. I've assumed this was + // a bug. + Quality.Poor => 1, + Quality.Awful => random.Next(1, 3), + _ => 0 + }; + + ActionResult TryDodge(RiskLevel riskLevel) + { + var difficultyModifier = riskLevel switch + { + RiskLevel.High => 3.0, + RiskLevel.Medium => 2.0, + _ => 0.5 + }; + + var outcome = (bullStrength + (difficultyModifier / 10)) * random.NextDouble() / + ((assistanceLevel + (passNumber / 10.0)) * 5); + + if (outcome < 0.51) + { + style += difficultyModifier; + return ActionResult.FightContinues; + } + else + return ActionResult.BullGoresPlayer; + } + + ActionResult TryKill(RiskLevel riskLevel) + { + var luck = bullStrength * 10 * random.NextDouble() / (assistanceLevel * 5 * passNumber); + + return ((riskLevel == RiskLevel.High && luck > 0.2) || luck > 0.8) ? + ActionResult.BullGoresPlayer : ActionResult.PlayerKillsBull; + } + + ActionResult Panic() => + ActionResult.BullGoresPlayer; + + ActionResult TrySurvive() + { + if (random.Next(2) == 0) + { + bravery = 1.5; + return ActionResult.BullKillsPlayer; + } + else + return ActionResult.FightContinues; + } + + ActionResult Flee() + { + bravery = 0.0; + return ActionResult.PlayerFlees; + } + + ActionResult IgnoreInjury(Action action) + { + if (random.Next(2) == 0) + { + bravery = 2.0; + return action == Action.Dodge ? ActionResult.FightContinues : ActionResult.Draw; + } + else + return ActionResult.BullGoresPlayer; + } + + Reward GetReward() + { + var score = CalculateScore(); + + if (score * random.NextDouble() < 2.4) + return Reward.Nothing; + else + if (score * random.NextDouble() < 4.9) + return Reward.OneEar; + else + if (score * random.NextDouble() < 7.4) + return Reward.TwoEars; + else + return Reward.CarriedFromRing; + } + + double CalculateScore() + { + var score = 4.5; + + // Style + score += style / 6; + + // Assisstance + score -= assistanceLevel * 2.5; + + // Courage + score += 4 * bravery; + + // Kill bonus + score += (result == ActionResult.PlayerKillsBull) ? 4 : 2; + + // Match length + score -= Math.Pow(passNumber, 2) / 120; + + // Difficulty + score -= (int)bullQuality; + + return score; + } + } + } +} diff --git a/17 Bullfight/csharp/src/Controller.cs b/17 Bullfight/csharp/src/Controller.cs index 2355e247..c1441f9a 100644 --- a/17 Bullfight/csharp/src/Controller.cs +++ b/17 Bullfight/csharp/src/Controller.cs @@ -93,10 +93,15 @@ namespace Game /// /// True if the player flees; otherwise, false. /// - public static bool PlayerRunsFromRing() + public static bool GetPlayerRunsFromRing() { View.PromptRunFromRing(); - return GetYesOrNo(); + + var playerFlees = GetYesOrNo(); + if (!playerFlees) + View.ShowPlayerFoolhardy(); + + return playerFlees; } /// diff --git a/17 Bullfight/csharp/src/Events/BullCharging.cs b/17 Bullfight/csharp/src/Events/BullCharging.cs new file mode 100644 index 00000000..bc041c64 --- /dev/null +++ b/17 Bullfight/csharp/src/Events/BullCharging.cs @@ -0,0 +1,7 @@ +namespace Game.Events +{ + /// + /// Indicates that the bull is charing the player. + /// + public sealed record BullCharging(int PassNumber) : Event; +} diff --git a/17 Bullfight/csharp/src/Events/Event.cs b/17 Bullfight/csharp/src/Events/Event.cs new file mode 100644 index 00000000..e9a8a646 --- /dev/null +++ b/17 Bullfight/csharp/src/Events/Event.cs @@ -0,0 +1,7 @@ +namespace Game.Events +{ + /// + /// Common base class for all events in the game. + /// + public abstract record Event(); +} diff --git a/17 Bullfight/csharp/src/Events/MatchCompleted.cs b/17 Bullfight/csharp/src/Events/MatchCompleted.cs new file mode 100644 index 00000000..94c67d06 --- /dev/null +++ b/17 Bullfight/csharp/src/Events/MatchCompleted.cs @@ -0,0 +1,7 @@ +namespace Game.Events +{ + /// + /// Indicates that the fight has completed. + /// + public sealed record MatchCompleted(ActionResult Result, bool ExtremeBravery, Reward Reward) : Event; +} diff --git a/17 Bullfight/csharp/src/Events/MatchStarted.cs b/17 Bullfight/csharp/src/Events/MatchStarted.cs new file mode 100644 index 00000000..8141b617 --- /dev/null +++ b/17 Bullfight/csharp/src/Events/MatchStarted.cs @@ -0,0 +1,13 @@ +namespace Game.Events +{ + /// + /// Indicates that a new match has started. + /// + public sealed record MatchStarted( + Quality BullQuality, + Quality ToreadorePerformance, + Quality PicadorePerformance, + int ToreadoresKilled, + int PicadoresKilled, + int HorsesKilled) : Event; +} diff --git a/17 Bullfight/csharp/src/Events/PlayerGored.cs b/17 Bullfight/csharp/src/Events/PlayerGored.cs new file mode 100644 index 00000000..f55ae20d --- /dev/null +++ b/17 Bullfight/csharp/src/Events/PlayerGored.cs @@ -0,0 +1,7 @@ +namespace Game.Events +{ + /// + /// Indicates that the player has been gored by the bull. + /// + public sealed record PlayerGored(bool Panicked, bool FirstGoring) : Event; +} diff --git a/17 Bullfight/csharp/src/Events/PlayerSurvived.cs b/17 Bullfight/csharp/src/Events/PlayerSurvived.cs new file mode 100644 index 00000000..135a9a09 --- /dev/null +++ b/17 Bullfight/csharp/src/Events/PlayerSurvived.cs @@ -0,0 +1,7 @@ +namespace Game.Events +{ + /// + /// Indicates that the player has survived being gored by the bull. + /// + public sealed record PlayerSurvived() : Event; +} diff --git a/17 Bullfight/csharp/src/MatchConditions.cs b/17 Bullfight/csharp/src/MatchConditions.cs deleted file mode 100644 index 5d5d979c..00000000 --- a/17 Bullfight/csharp/src/MatchConditions.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Game -{ - /// - /// Stores the initial conditions of a match. - /// - public record MatchConditions - { - /// - /// Gets the quality of the bull. - /// - public Quality BullQuality { get; init; } - - /// - /// Gets the quality of help received from the toreadores. - /// - public Quality ToreadorePerformance { get; init; } - - /// - /// Gets the quality of help received from the picadores. - /// - public Quality PicadorePerformance { get; init; } - - /// - /// Gets the number of toreadores killed while preparing for the - /// final round. - /// - public int ToreadoresKilled { get; init; } - - /// - /// Gets the number of picadores killed while preparing for the - /// final round. - /// - public int PicadoresKilled { get; init; } - - /// - /// Gets the number of horses killed while preparing for the final - /// round. - /// - public int HorsesKilled { get; init; } - } -} diff --git a/17 Bullfight/csharp/src/MatchState.cs b/17 Bullfight/csharp/src/MatchState.cs deleted file mode 100644 index 3eae6d5d..00000000 --- a/17 Bullfight/csharp/src/MatchState.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Game -{ - /// - /// Stores the current state of the match. - /// - public record MatchState(MatchConditions Conditions) - { - /// - /// Gets the number of times the bull has charged. - /// - public int PassNumber { get; init; } - - /// - /// Measures the player's bravery during the match. - /// - public double Bravery { get; init; } - - /// - /// Measures how much style the player showed during the match. - /// - public double Style { get; init; } - - /// - /// Gets the result of the player's last action. - /// - public ActionResult Result { get; init; } - } -} diff --git a/17 Bullfight/csharp/src/Mediator.cs b/17 Bullfight/csharp/src/Mediator.cs new file mode 100644 index 00000000..c9d4e298 --- /dev/null +++ b/17 Bullfight/csharp/src/Mediator.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace Game +{ + /// + /// Facilitates sending messages between the two game loops. + /// + /// + /// This class serves as a little piece of glue in between the main program + /// loop and the bull fight coroutine. When the main program calls one of + /// its methods, the mediator creates the appropriate input data that the + /// bull fight coroutine later retrieves with . + /// + public class Mediator + { + private object? m_input; + + public void Dodge(RiskLevel riskLevel) => + m_input = (Action.Dodge, riskLevel); + + public void Kill(RiskLevel riskLevel) => + m_input = (Action.Kill, riskLevel); + + public void Panic() => + m_input = (Action.Panic, default(RiskLevel)); + + public void RunFromRing() => + m_input = true; + + public void ContinueFighting() => + m_input = false; + + /// + /// Gets the next input from the user. + /// + /// + /// The type of input to receive. + /// + public T GetInput() + { + Debug.Assert(m_input is not null, "No input received"); + Debug.Assert(m_input.GetType() == typeof(T), "Invalid input received"); + var result = (T)m_input; + m_input = null; + return result; + } + } +} diff --git a/17 Bullfight/csharp/src/Program.cs b/17 Bullfight/csharp/src/Program.cs index 17da736f..d3f3760b 100644 --- a/17 Bullfight/csharp/src/Program.cs +++ b/17 Bullfight/csharp/src/Program.cs @@ -1,6 +1,4 @@ -using System; - -namespace Game +namespace Game { class Program { @@ -8,49 +6,49 @@ namespace Game { Controller.StartGame(); - var random = new Random(); - var match = Rules.StartMatch(random); - View.ShowStartingConditions(match.Conditions); - - while (match.Result == ActionResult.FightContinues) + var mediator = new Mediator(); + foreach (var evt in BullFight.Begin(mediator)) { - match = match with { PassNumber = match.PassNumber + 1 }; - - View.StartOfPass(match.PassNumber); - - var (action, riskLevel) = Controller.GetPlayerIntention(match.PassNumber); - match = action switch + switch (evt) { - Action.Dodge => Rules.TryDodge(random, riskLevel, match), - Action.Kill => Rules.TryKill(random, riskLevel, match), - _ => Rules.Panic(match) - }; + case Events.MatchStarted matchStarted: + View.ShowStartingConditions(matchStarted); + break; - var first = true; - while (match.Result == ActionResult.BullGoresPlayer) - { - View.ShowPlayerGored(action == Action.Panic, first); - first = false; + case Events.BullCharging bullCharging: + View.ShowStartOfPass(bullCharging.PassNumber); + var (action, riskLevel) = Controller.GetPlayerIntention(bullCharging.PassNumber); + switch (action) + { + case Action.Dodge: + mediator.Dodge(riskLevel); + break; + case Action.Kill: + mediator.Kill(riskLevel); + break; + case Action.Panic: + mediator.Panic(); + break; + } + break; - match = Rules.TrySurvive(random, match); - if (match.Result == ActionResult.FightContinues) - { + case Events.PlayerGored playerGored: + View.ShowPlayerGored(playerGored.Panicked, playerGored.FirstGoring); + break; + + case Events.PlayerSurvived: View.ShowPlayerSurvives(); - - if (Controller.PlayerRunsFromRing()) - { - match = Rules.Flee(match); - } + if (Controller.GetPlayerRunsFromRing()) + mediator.RunFromRing(); else - { - View.ShowPlayerFoolhardy(); - match = Rules.IgnoreInjury(random, action, match); - } - } + mediator.ContinueFighting(); + break; + + case Events.MatchCompleted matchCompleted: + View.ShowFinalResult(matchCompleted.Result, matchCompleted.ExtremeBravery, matchCompleted.Reward); + break; } } - - View.ShowFinalResult(match.Result, match.Bravery, Rules.GetReward(random, match)); } } } diff --git a/17 Bullfight/csharp/src/Rules.cs b/17 Bullfight/csharp/src/Rules.cs deleted file mode 100644 index 463af1ed..00000000 --- a/17 Bullfight/csharp/src/Rules.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; - -namespace Game -{ - /// - /// Provides functions implementing the rules of the game. - /// - public static class Rules - { - /// - /// Gets the state of a new match. - /// - /// - /// The random number generator. - /// - public static MatchState StartMatch(Random random) - { - var bullQuality = GetBullQuality(); - var toreadorePerformance = GetHelpQuality(); - var picadorePerformance = GetHelpQuality(); - - var conditions = new MatchConditions - { - BullQuality = bullQuality, - ToreadorePerformance = toreadorePerformance, - PicadorePerformance = picadorePerformance, - ToreadoresKilled = GetHumanCasualties(toreadorePerformance), - PicadoresKilled = GetHumanCasualties(picadorePerformance), - HorsesKilled = GetHorseCasualties(picadorePerformance) - }; - - return new MatchState(conditions) - { - Bravery = 1.0, - Style = 1.0 - }; - - Quality GetBullQuality() => - (Quality)random.Next(1, 6); - - Quality GetHelpQuality() => - ((3.0 / (int)bullQuality) * random.NextDouble()) switch - { - < 0.37 => Quality.Superb, - < 0.50 => Quality.Good, - < 0.63 => Quality.Fair, - < 0.87 => Quality.Poor, - _ => Quality.Awful - }; - - int GetHumanCasualties(Quality performance) => - performance switch - { - Quality.Poor => random.Next(0, 2), - Quality.Awful => random.Next(1, 3), - _ => 0 - }; - - int GetHorseCasualties(Quality performance) => - performance switch - { - // NOTE: The code for displaying a single horse casuality - // following a poor picadore peformance was unreachable - // in the original BASIC version. I've assumed this was - // a bug. - Quality.Poor => 1, - Quality.Awful => random.Next(1, 3), - _ => 0 - }; - } - - /// - /// Determines the result when the player attempts to dodge the bull. - /// - /// - /// The random number generator. - /// - /// - /// The level of risk in the dodge manoeuvre chosen. - /// - /// - /// The current match state. - /// - /// - /// The updated match state. - /// - public static MatchState TryDodge(Random random, RiskLevel riskLevel, MatchState match) - { - var difficultyModifier = riskLevel switch - { - RiskLevel.High => 3.0, - RiskLevel.Medium => 2.0, - _ => 0.5 - }; - - var outcome = (GetBullStrength(match) + (difficultyModifier / 10)) * random.NextDouble() / - ((GetAssisstance(match) + (match.PassNumber / 10.0)) * 5); - - return outcome < 0.51 ? - match with { Result = ActionResult.FightContinues, Style = match.Style + difficultyModifier } : - match with { Result = ActionResult.BullGoresPlayer }; - } - - /// - /// Determines the result when the player attempts to kill the bull. - /// - /// - /// The random number generator. - /// - /// - /// The level of risk in the manoeuvre chosen. - /// - /// - /// The current match state. - /// - /// - /// The updated match state. - /// - public static MatchState TryKill(Random random, RiskLevel riskLevel, MatchState match) - { - var K = GetBullStrength(match) * 10 * random.NextDouble() / (GetAssisstance(match) * 5 * match.PassNumber); - - return ((riskLevel == RiskLevel.High && K > 0.2) || K > 0.8) ? - match with { Result = ActionResult.BullGoresPlayer } : - match with { Result = ActionResult.PlayerKillsBull }; - } - - /// - /// Determines if the player survives being gored by the bull. - /// - /// - /// The random number generator. - /// - /// - /// The current match state. - /// - /// - /// The updated match state. - /// - public static MatchState TrySurvive(Random random, MatchState match) => - (random.Next(2) == 0) ? - match with { Result = ActionResult.BullKillsPlayer, Bravery = 1.5 } : - match with { Result = ActionResult.FightContinues }; - - /// - /// Determines the result when the player panics and fails to do anything. - /// - /// - /// The match state. - /// - public static MatchState Panic(MatchState match) => - match with { Result = ActionResult.BullGoresPlayer }; - - /// - /// Determines the result when the player flees the ring. - /// - /// - /// The current match state. - /// - /// - /// The updated match state. - /// - public static MatchState Flee(MatchState match) => - match with { Result = ActionResult.PlayerFlees, Bravery = 0.0 }; - - /// - /// Determines the result when the player decides to continue fighting - /// following an injury. - /// - /// - /// The random number generator. - /// - /// - /// The action the player took that lead to the injury. - /// - /// - /// The current match state. - /// - /// - /// The updated match state. - /// - public static MatchState IgnoreInjury(Random random, Action action, MatchState match) => - (random.Next(2) == 0) ? - match with { Result = action == Action.Dodge ? ActionResult.FightContinues : ActionResult.Draw, Bravery = 2.0 } : - match with { Result = ActionResult.BullGoresPlayer }; - - /// - /// Gets the player's reward for completing a match. - /// - /// - /// The random number generator. - /// - /// - /// The final match state. - /// - public static Reward GetReward(Random random, MatchState match) - { - var score = CalculateScore(); - - if (score * random.NextDouble() < 2.4) - return Reward.Nothing; - else - if (score * random.NextDouble() < 4.9) - return Reward.OneEar; - else - if (score * random.NextDouble() < 7.4) - return Reward.TwoEars; - else - return Reward.CarriedFromRing; - - double CalculateScore() - { - var score = 4.5; - - // Style - score += match.Style / 6; - - // Assisstance - score -= GetAssisstance(match) * 2.5; - - // Courage - score += 4 * match.Bravery; - - // Kill bonus - score += (match.Result == ActionResult.PlayerKillsBull) ? 4 : 2; - - // Match length - score -= Math.Pow(match.PassNumber, 2) / 120; - - // Difficulty - score -= (int)match.Conditions.BullQuality; - - return score; - } - } - - /// - /// Calculates the strength of the bull in a match. - /// - private static double GetBullStrength(MatchState match) => - 6 - (int)match.Conditions.BullQuality; - - /// - /// Gets the amount of assistance received from the toreadores and - /// picadores in a match. - /// - private static double GetAssisstance(MatchState match) => - GetPerformanceBonus(match.Conditions.ToreadorePerformance) + - GetPerformanceBonus(match.Conditions.PicadorePerformance); - - /// - /// Gets the amount of assistance rendered by a performance of the - /// given quality. - /// - private static double GetPerformanceBonus(Quality performance) => - (6 - (int)performance) * 0.1; - } -} diff --git a/17 Bullfight/csharp/src/View.cs b/17 Bullfight/csharp/src/View.cs index 2bff39c8..0cc627b8 100644 --- a/17 Bullfight/csharp/src/View.cs +++ b/17 Bullfight/csharp/src/View.cs @@ -47,22 +47,22 @@ namespace Game Console.WriteLine(); } - public static void ShowStartingConditions(MatchConditions conditions) + public static void ShowStartingConditions(Events.MatchStarted matchStarted) { ShowBullQuality(); - ShowHelpQuality("TOREADORES", conditions.ToreadorePerformance, conditions.ToreadoresKilled, 0); - ShowHelpQuality("PICADORES", conditions.PicadorePerformance, conditions.PicadoresKilled, conditions.HorsesKilled); + ShowHelpQuality("TOREADORES", matchStarted.ToreadorePerformance, matchStarted.ToreadoresKilled, 0); + ShowHelpQuality("PICADORES", matchStarted.PicadorePerformance, matchStarted.PicadoresKilled, matchStarted.HorsesKilled); void ShowBullQuality() { - Console.WriteLine($"YOU HAVE DRAWN A {QualityString[(int)conditions.BullQuality - 1]} BULL."); + Console.WriteLine($"YOU HAVE DRAWN A {QualityString[(int)matchStarted.BullQuality - 1]} BULL."); - if (conditions.BullQuality > Quality.Poor) + if (matchStarted.BullQuality > Quality.Poor) { Console.WriteLine("YOU'RE LUCKY"); } else - if (conditions.BullQuality < Quality.Good) + if (matchStarted.BullQuality < Quality.Good) { Console.WriteLine("GOOD LUCK. YOU'LL NEED IT."); Console.WriteLine(); @@ -86,9 +86,11 @@ namespace Game case Quality.Poor: if (horsesKilled > 0) Console.WriteLine($"ONE OF THE HORSES OF THE {helperName} WAS KILLED."); - + if (helpersKilled > 0) Console.WriteLine($"ONE OF THE {helperName} WAS KILLED."); + else + Console.WriteLine($"NO {helperName} WERE KILLED."); break; case Quality.Awful: @@ -101,7 +103,7 @@ namespace Game } } - public static void StartOfPass(int passNumber) + public static void ShowStartOfPass(int passNumber) { Console.WriteLine(); Console.WriteLine(); @@ -129,7 +131,7 @@ namespace Game Console.WriteLine("YOU ARE BRAVE. STUPID, BUT BRAVE."); } - public static void ShowFinalResult(ActionResult result, double bravery, Reward reward) + public static void ShowFinalResult(ActionResult result, bool extremeBravery, Reward reward) { Console.WriteLine(); Console.WriteLine(); @@ -156,7 +158,7 @@ namespace Game } else { - if (bravery == 2) // You were gored by the bull but survived (and did not later die or flee) + if (extremeBravery) Console.WriteLine("THE CROWD CHEERS WILDLY!"); else if (result == ActionResult.PlayerKillsBull)