I am building a game where two players have to pronounce name of a city. The main rule is better to explain with the following example:
- computer starts with "New York"
- player tells "Kopenhagen", as he needs to tell a name which starts from "K", because "K" is the last letter in "New York"
- computer tells "Nairobi" and so on and so forth. Any city name cannot be mentioned twice.
When a player doesn't know any city name which starts from the appropriate letter, he leaves the game. The winner is apparently the last man standing.
Below is the code I've written for rules engine of the game. I make it staless, so the previous turns and the entire computer knowledge base caches are AppService layer responsibility.
Also there is a need to verify if user doesn't put a name which is not a city at all. Currently I left such a function as input parameter _validateCityExists
.
public class GameCitiesRules
{
private readonly Func<string, bool> _validateCityExists;
private static IEnumerable<string> _cityNamesKnowledgeBase;
private static IEnumerable<string> _previousTurns;
//none word can start with these letters. So if a city name ends with such a letter - we use the second one from the end as target letter
private static readonly IEnumerable<char> NonMeaningfulLetters = new List<char>()
{
'Ы',
'Ъ',
'Ь',
};
protected GameCitiesRules(IEnumerable<string> previousTurns,
IEnumerable<string> cityNamesKnowledgeBase,
Func<string, bool> validateCityExists)
{
_cityNamesKnowledgeBase = cityNamesKnowledgeBase.Select(c => c.ToUpper(CultureInfo.InvariantCulture));
_previousTurns = previousTurns.Select(s => s.ToUpper(CultureInfo.InvariantCulture));
_validateCityExists = validateCityExists;
}
public static GameCitiesRules Create(IEnumerable<string> previousTurns,
IEnumerable<string> cityNamesKnowledgeBase,
Func<string, bool> validateCityExists)
{
Require.ArgNotNull(previousTurns, "previous turns");
Require.ArgNotNull(cityNamesKnowledgeBase, "city names knowledge base");
Require.That(() => cityNamesKnowledgeBase.Any(), "knowledge base cannot be empty");
Require.ArgNotNull(validateCityExists, "city verification function");
return new GameCitiesRules(previousTurns, cityNamesKnowledgeBase, validateCityExists);
}
public void StartNew(bool computerFirst, out string answer)
{
Require.That(() => !_previousTurns.Any(), "Game cannot be started twice");
if (computerFirst)
{
var firstWordIndex = new Random((int)DateTime.Now.Ticks)
.Next(_cityNamesKnowledgeBase.Count());
answer = _cityNamesKnowledgeBase.ElementAt(firstWordIndex);
}
else
{
answer = string.Empty;
}
}
// can throw GameRuleViolationException, so client can catch and handle it
public bool TryExecute(string userInput,
out string answer)
{
Require.NotNullOrWhitespace(userInput, "city name");
userInput = userInput.ToUpper(CultureInfo.InvariantCulture);
GameRequire.That(() =>
{
if (!_previousTurns.Any())
{
return true;
}
var letter = userInput.FirstLetter();
var desiredLetter = GetLastMeaningfulLetter(_previousTurns.Last());
return letter == desiredLetter;
},
"user stated city which starts with wrong letter",
GamesViolationCodes.CitiesStartsWithWrongLetter);
GameRequire.That(() => !_previousTurns.Contains(userInput),
"user stated city which had already been mentioned",
GamesViolationCodes.CitiesAlreadyMentioned);
GameRequire.That(() => _validateCityExists(userInput),
"user stated an unrecognized city",
GamesViolationCodes.CitiesNotACity);
var newDesiredLetter = GetLastMeaningfulLetter(userInput).ToString();
answer = _cityNamesKnowledgeBase.FirstOrDefault(x => x.StartsWith(newDesiredLetter)
&& !_previousTurns.Contains(x)
&& !string.Equals(userInput, x));
return !string.IsNullOrEmpty(answer);
}
// returns the current desired letter
public string Explain()
{
if (_previousTurns.Any())
{
return GetLastMeaningfulLetter(_previousTurns.Last()).ToString();
}
return string.Empty;
}
protected char GetLastMeaningfulLetter(string word)
{
return word.Last(letter => !NonMeaningfulLetters.Contains(letter));
}
}
There are a few helper classes to arrange multiple checks required in the game:
public class GameRequire
{
public static void That(Func<bool> condition,
string errorMesssage,
GamesViolationCodes violationCode)
{
if (condition == null)
{
throw new InvalidOperationException("Game rule condition must be set");
}
if (!condition())
{
throw new GameRulesViolationException(errorMesssage, violationCode);
}
}
}
public enum GamesViolationCodes
{
CitiesStartsWithWrongLetter = 0,
CitiesAlreadyMentioned = 1,
CitiesNotACity = 2,
}
public class Require
{
public static void That(bool condition, string errorMessage)
{
if (!condition)
{
throw new DomainRuleViolationException(errorMessage);
}
}
public static void That(Func<bool> condition, string errorMessage)
{
if (condition == null)
{
throw new InvalidOperationException("Domain rule condition must be set");
}
if (!condition())
{
throw new DomainRuleViolationException(errorMessage);
}
}
}