Initial port of Bullfight to C#

This commit is contained in:
Peter
2021-07-22 16:55:23 -04:00
parent 81e36428b2
commit bc628e71da
13 changed files with 896 additions and 0 deletions

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31321.278
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Game", "Game.csproj", "{8F7C450E-5F3A-45BA-9DB9-329744214931}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8F7C450E-5F3A-45BA-9DB9-329744214931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F7C450E-5F3A-45BA-9DB9-329744214931}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F7C450E-5F3A-45BA-9DB9-329744214931}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F7C450E-5F3A-45BA-9DB9-329744214931}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5BFC749-C7D8-4981-A7D4-1D401901A890}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,24 @@
namespace Game
{
/// <summary>
/// Enumerates the different actions that the player can take on each round
/// of the fight.
/// </summary>
public enum Action
{
/// <summary>
/// Dodge the bull.
/// </summary>
Dodge,
/// <summary>
/// Kill the bull.
/// </summary>
Kill,
/// <summary>
/// Freeze in place and don't do anything.
/// </summary>
Panic
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Game
{
/// <summary>
/// Enumerates the different possible outcomes of the player's action.
/// </summary>
public enum ActionResult
{
/// <summary>
/// The fight continues.
/// </summary>
FightContinues,
/// <summary>
/// The player fled from the ring.
/// </summary>
PlayerFlees,
/// <summary>
/// The bull has gored the player.
/// </summary>
BullGoresPlayer,
/// <summary>
/// The bull killed the player.
/// </summary>
BullKillsPlayer,
/// <summary>
/// The player killed the bull.
/// </summary>
PlayerKillsBull,
/// <summary>
/// The player attempted to kill the bull and both survived.
/// </summary>
Draw
}
}

View File

@@ -0,0 +1,129 @@
using System;
namespace Game
{
/// <summary>
/// Contains functions for getting input from the user.
/// </summary>
public static class Controller
{
/// <summary>
/// Handles the initial interaction with the player.
/// </summary>
public static void StartGame()
{
View.ShowBanner();
View.PromptShowInstructions();
var input = Console.ReadLine();
if (input is null)
Environment.Exit(0);
if (input.ToUpperInvariant() != "NO")
View.ShowInstructions();
View.ShowSeparator();
}
/// <summary>
/// Gets the player's action for the current round.
/// </summary>
/// <param name="passNumber">
/// The current pass number.
/// </param>
public static (Action action, RiskLevel riskLevel) GetPlayerIntention(int passNumber)
{
if (passNumber < 3)
View.PromptKillBull();
else
View.PromptKillBullBrief();
var attemptToKill = GetYesOrNo();
if (attemptToKill)
{
View.PromptKillMethod();
var input = Console.ReadLine();
if (input is null)
Environment.Exit(0);
return input switch
{
"4" => (Action.Kill, RiskLevel.High),
"5" => (Action.Kill, RiskLevel.Low),
_ => (Action.Panic, default(RiskLevel))
};
}
else
{
if (passNumber < 2)
View.PromptCapeMove();
else
View.PromptCapeMoveBrief();
var action = Action.Panic;
var riskLevel = default(RiskLevel);
while (action == Action.Panic)
{
var input = Console.ReadLine();
if (input is null)
Environment.Exit(0);
(action, riskLevel) = input switch
{
"0" => (Action.Dodge, RiskLevel.High),
"1" => (Action.Dodge, RiskLevel.Medium),
"2" => (Action.Dodge, RiskLevel.Low),
_ => (Action.Panic, default(RiskLevel))
};
if (action == Action.Panic)
View.PromptDontPanic();
}
return (action, riskLevel);
}
}
/// <summary>
/// Gets the player's intention to flee (or not).
/// </summary>
/// <returns>
/// True if the player flees; otherwise, false.
/// </returns>
public static bool PlayerRunsFromRing()
{
View.PromptRunFromRing();
return GetYesOrNo();
}
/// <summary>
/// Gets a yes or no response from the player.
/// </summary>
/// <returns>
/// True if the user answered yes; otherwise, false.
/// </returns>
public static bool GetYesOrNo()
{
while (true)
{
var input = Console.ReadLine();
if (input is null)
Environment.Exit(0);
switch (input.ToUpperInvariant())
{
case "YES":
return true;
case "NO":
return false;
default:
Console.WriteLine("INCORRECT ANSWER - - PLEASE TYPE 'YES' OR 'NO'.");
break;
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
namespace Game
{
/// <summary>
/// Stores the initial conditions of a match.
/// </summary>
public record MatchConditions
{
/// <summary>
/// Gets the quality of the bull.
/// </summary>
public Quality BullQuality { get; init; }
/// <summary>
/// Gets the quality of help received from the toreadores.
/// </summary>
public Quality ToreadorePerformance { get; init; }
/// <summary>
/// Gets the quality of help received from the picadores.
/// </summary>
public Quality PicadorePerformance { get; init; }
/// <summary>
/// Gets the number of toreadores killed while preparing for the
/// final round.
/// </summary>
public int ToreadoresKilled { get; init; }
/// <summary>
/// Gets the number of picadores killed while preparing for the
/// final round.
/// </summary>
public int PicadoresKilled { get; init; }
/// <summary>
/// Gets the number of horses killed while preparing for the final
/// round.
/// </summary>
public int HorsesKilled { get; init; }
}
}

View File

@@ -0,0 +1,28 @@
namespace Game
{
/// <summary>
/// Stores the current state of the match.
/// </summary>
public record MatchState(MatchConditions Conditions)
{
/// <summary>
/// Gets the number of times the bull has charged.
/// </summary>
public int PassNumber { get; init; }
/// <summary>
/// Measures the player's bravery during the match.
/// </summary>
public double Bravery { get; init; }
/// <summary>
/// Measures how much style the player showed during the match.
/// </summary>
public double Style { get; init; }
/// <summary>
/// Gets the result of the player's last action.
/// </summary>
public ActionResult Result { get; init; }
}
}

View File

@@ -0,0 +1,56 @@
using System;
namespace Game
{
class Program
{
static void Main()
{
Controller.StartGame();
var random = new Random();
var match = Rules.StartMatch(random);
View.ShowStartingConditions(match.Conditions);
while (match.Result == ActionResult.FightContinues)
{
match = match with { PassNumber = match.PassNumber + 1 };
View.StartOfPass(match.PassNumber);
var (action, riskLevel) = Controller.GetPlayerIntention(match.PassNumber);
match = action switch
{
Action.Dodge => Rules.TryDodge(random, riskLevel, match),
Action.Kill => Rules.TryKill(random, riskLevel, match),
_ => Rules.Panic(match)
};
var first = true;
while (match.Result == ActionResult.BullGoresPlayer)
{
View.ShowPlayerGored(action == Action.Panic, first);
first = false;
match = Rules.TrySurvive(random, match);
if (match.Result == ActionResult.FightContinues)
{
View.ShowPlayerSurvives();
if (Controller.PlayerRunsFromRing())
{
match = Rules.Flee(match);
}
else
{
View.ShowPlayerFoolhardy();
match = Rules.IgnoreInjury(random, action, match);
}
}
}
}
View.ShowFinalResult(match.Result, match.Bravery, Rules.GetReward(random, match));
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Game
{
/// <summary>
/// Enumerates the different levels of quality in the game.
/// </summary>
/// <remarks>
/// Quality applies both to the bull and to the help received from the
/// toreadores and picadores. Note that the ordinal values are significant
/// (these are used in various calculations).
/// </remarks>
public enum Quality
{
Superb = 1,
Good = 2,
Fair = 3,
Poor = 4,
Awful = 5
}
}

View File

@@ -0,0 +1,13 @@
namespace Game
{
/// <summary>
/// Enumerates the different things the player can be awarded.
/// </summary>
public enum Reward
{
Nothing,
OneEar,
TwoEars,
CarriedFromRing
}
}

View File

@@ -0,0 +1,12 @@
namespace Game
{
/// <summary>
/// Enumerates the different levels of risk for manoeuvres in the game.
/// </summary>
public enum RiskLevel
{
Low,
Medium,
High
}
}

View File

@@ -0,0 +1,258 @@
using System;
namespace Game
{
/// <summary>
/// Provides functions implementing the rules of the game.
/// </summary>
public static class Rules
{
/// <summary>
/// Gets the state of a new match.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
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
};
}
/// <summary>
/// Determines the result when the player attempts to dodge the bull.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="riskLevel">
/// The level of risk in the dodge manoeuvre chosen.
/// </param>
/// <param name="match">
/// The current match state.
/// </param>
/// <returns>
/// The updated match state.
/// </returns>
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 };
}
/// <summary>
/// Determines the result when the player attempts to kill the bull.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="riskLevel">
/// The level of risk in the manoeuvre chosen.
/// </param>
/// <param name="match">
/// The current match state.
/// </param>
/// <returns>
/// The updated match state.
/// </returns>
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 };
}
/// <summary>
/// Determines if the player survives being gored by the bull.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="match">
/// The current match state.
/// </param>
/// <returns>
/// The updated match state.
/// </returns>
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 };
/// <summary>
/// Determines the result when the player panics and fails to do anything.
/// </summary>
/// <param name="match">
/// The match state.
/// </param>
public static MatchState Panic(MatchState match) =>
match with { Result = ActionResult.BullGoresPlayer };
/// <summary>
/// Determines the result when the player flees the ring.
/// </summary>
/// <param name="match">
/// The current match state.
/// </param>
/// <returns>
/// The updated match state.
/// </returns>
public static MatchState Flee(MatchState match) =>
match with { Result = ActionResult.PlayerFlees, Bravery = 0.0 };
/// <summary>
/// Determines the result when the player decides to continue fighting
/// following an injury.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="action">
/// The action the player took that lead to the injury.
/// </param>
/// <param name="match">
/// The current match state.
/// </param>
/// <returns>
/// The updated match state.
/// </returns>
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 };
/// <summary>
/// Gets the player's reward for completing a match.
/// </summary>
/// <param name="random">
/// The random number generator.
/// </param>
/// <param name="match">
/// The final match state.
/// </param>
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;
}
}
/// <summary>
/// Calculates the strength of the bull in a match.
/// </summary>
private static double GetBullStrength(MatchState match) =>
6 - (int)match.Conditions.BullQuality;
/// <summary>
/// Gets the amount of assistance received from the toreadores and
/// picadores in a match.
/// </summary>
private static double GetAssisstance(MatchState match) =>
GetPerformanceBonus(match.Conditions.ToreadorePerformance) +
GetPerformanceBonus(match.Conditions.PicadorePerformance);
/// <summary>
/// Gets the amount of assistance rendered by a performance of the
/// given quality.
/// </summary>
private static double GetPerformanceBonus(Quality performance) =>
(6 - (int)performance) * 0.1;
}
}

View File

@@ -0,0 +1,240 @@
using System;
namespace Game
{
/// <summary>
/// Contains functions for displaying information to the user.
/// </summary>
public static class View
{
private static readonly string[] QualityString = { "SUPERB", "GOOD", "FAIR", "POOR", "AWFUL" };
public static void ShowBanner()
{
Console.WriteLine(" BULL");
Console.WriteLine(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
public static void ShowInstructions()
{
Console.WriteLine("HELLO, ALL YOU BLOODLOVERS AND AFICIONADOS.");
Console.WriteLine("HERE IS YOUR BIG CHANCE TO KILL A BULL.");
Console.WriteLine();
Console.WriteLine("ON EACH PASS OF THE BULL, YOU MAY TRY");
Console.WriteLine("0 - VERONICA (DANGEROUS INSIDE MOVE OF THE CAPE)");
Console.WriteLine("1 - LESS DANGEROUS OUTSIDE MOVE OF THE CAPE");
Console.WriteLine("2 - ORDINARY SWIRL OF THE CAPE.");
Console.WriteLine();
Console.WriteLine("INSTEAD OF THE ABOVE, YOU MAY TRY TO KILL THE BULL");
Console.WriteLine("ON ANY TURN: 4 (OVER THE HORNS), 5 (IN THE CHEST).");
Console.WriteLine("BUT IF I WERE YOU,");
Console.WriteLine("I WOULDN'T TRY IT BEFORE THE SEVENTH PASS.");
Console.WriteLine();
Console.WriteLine("THE CROWD WILL DETERMINE WHAT AWARD YOU DESERVE");
Console.WriteLine("(POSTHUMOUSLY IF NECESSARY).");
Console.WriteLine("THE BRAVER YOU ARE, THE BETTER THE AWARD YOU RECEIVE.");
Console.WriteLine();
Console.WriteLine("THE BETTER THE JOB THE PICADORES AND TOREADORES DO,");
Console.WriteLine("THE BETTER YOUR CHANCES ARE.");
}
public static void ShowSeparator()
{
Console.WriteLine();
Console.WriteLine();
}
public static void ShowStartingConditions(MatchConditions conditions)
{
ShowBullQuality();
ShowHelpQuality("TOREADORES", conditions.ToreadorePerformance, conditions.ToreadoresKilled, 0);
ShowHelpQuality("PICADORES", conditions.PicadorePerformance, conditions.PicadoresKilled, conditions.HorsesKilled);
void ShowBullQuality()
{
Console.WriteLine($"YOU HAVE DRAWN A {QualityString[(int)conditions.BullQuality - 1]} BULL.");
if (conditions.BullQuality > Quality.Poor)
{
Console.WriteLine("YOU'RE LUCKY");
}
else
if (conditions.BullQuality < Quality.Good)
{
Console.WriteLine("GOOD LUCK. YOU'LL NEED IT.");
Console.WriteLine();
}
Console.WriteLine();
}
static void ShowHelpQuality(string helperName, Quality helpQuality, int helpersKilled, int horsesKilled)
{
Console.WriteLine($"THE {helperName} DID A {QualityString[(int)helpQuality - 1]} JOB.");
// NOTE: The code below makes some *strong* assumptions about
// how the casualty numbers were generated. It is written
// this way to preserve the behaviour of the original BASIC
// version, but it would make more sense ignore the helpQuality
// parameter and just use the provided numbers to decide what
// to display.
switch (helpQuality)
{
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.");
break;
case Quality.Awful:
if (horsesKilled > 0)
Console.WriteLine($" {horsesKilled} OF THE HORSES OF THE {helperName} KILLED.");
Console.WriteLine($" {helpersKilled} OF THE {helperName} KILLED.");
break;
}
}
}
public static void StartOfPass(int passNumber)
{
Console.WriteLine();
Console.WriteLine();
Console.WriteLine($"PASS NUMBER {passNumber}");
}
public static void ShowPlayerGored(bool playerPanicked, bool firstGoring)
{
Console.WriteLine((playerPanicked, firstGoring) switch
{
(true, true) => "YOU PANICKED. THE BULL GORED YOU.",
(false, true) => "THE BULL HAS GORED YOU!",
(_, false) => "YOU ARE GORED AGAIN!"
});
}
public static void ShowPlayerSurvives()
{
Console.WriteLine("YOU ARE STILL ALIVE.");
Console.WriteLine();
}
public static void ShowPlayerFoolhardy()
{
Console.WriteLine("YOU ARE BRAVE. STUPID, BUT BRAVE.");
}
public static void ShowFinalResult(ActionResult result, double bravery, Reward reward)
{
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
switch (result)
{
case ActionResult.PlayerFlees:
Console.WriteLine("COWARD");
break;
case ActionResult.BullKillsPlayer:
Console.WriteLine("YOU ARE DEAD.");
break;
case ActionResult.PlayerKillsBull:
Console.WriteLine("YOU KILLED THE BULL!");
break;
}
if (result == ActionResult.PlayerFlees)
{
Console.WriteLine("THE CROWD BOOS FOR TEN MINUTES. IF YOU EVER DARE TO SHOW");
Console.WriteLine("YOUR FACE IN A RING AGAIN, THEY SWEAR THEY WILL KILL YOU--");
Console.WriteLine("UNLESS THE BULL DOES FIRST.");
}
else
{
if (bravery == 2) // You were gored by the bull but survived (and did not later die or flee)
Console.WriteLine("THE CROWD CHEERS WILDLY!");
else
if (result == ActionResult.PlayerKillsBull)
{
Console.WriteLine("THE CROWD CHEERS!");
Console.WriteLine();
}
Console.WriteLine("THE CROWD AWARDS YOU");
switch (reward)
{
case Reward.Nothing:
Console.WriteLine("NOTHING AT ALL.");
break;
case Reward.OneEar:
Console.WriteLine("ONE EAR OF THE BULL.");
break;
case Reward.TwoEars:
Console.WriteLine("BOTH EARS OF THE BULL!");
Console.WriteLine("OLE!");
break;
default:
Console.WriteLine("OLE! YOU ARE 'MUY HOMBRE'!! OLE! OLE!");
break;
}
}
Console.WriteLine();
Console.WriteLine("ADIOS");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
public static void PromptShowInstructions()
{
Console.Write("DO YOU WANT INSTRUCTIONS? ");
}
public static void PromptKillBull()
{
Console.WriteLine("THE BULL IS CHARGING AT YOU! YOU ARE THE MATADOR--");
Console.Write("DO YOU WANT TO KILL THE BULL? ");
}
public static void PromptKillBullBrief()
{
Console.Write("HERE COMES THE BULL. TRY FOR A KILL? ");
}
public static void PromptKillMethod()
{
Console.WriteLine();
Console.WriteLine("IT IS THE MOMENT OF TRUTH.");
Console.WriteLine();
Console.Write("HOW DO YOU TRY TO KILL THE BULL? ");
}
public static void PromptCapeMove()
{
Console.Write("WHAT MOVE DO YOU MAKE WITH THE CAPE? ");
}
public static void PromptCapeMoveBrief()
{
Console.Write("CAPE MOVE? ");
}
public static void PromptDontPanic()
{
Console.WriteLine("DON'T PANIC, YOU IDIOT! PUT DOWN A CORRECT NUMBER");
Console.Write("? ");
}
public static void PromptRunFromRing()
{
Console.Write("DO YOU RUN FROM THE RING? ");
}
}
}