diff --git a/04_Awari/csharp/Game.cs b/04_Awari/csharp/Game.cs new file mode 100644 index 00000000..8a673dea --- /dev/null +++ b/04_Awari/csharp/Game.cs @@ -0,0 +1,264 @@ +namespace Awari; + +public class Game +{ + public int[] PlayerPits => _beans[0..6]; + public int[] ComputerPits => _beans[7..13]; + public int PlayerHome => _beans[_playerHome]; + public int ComputerHome => _beans[_computerHome]; + + private bool IsDone => + PlayerPits.All(b => b == 0) // if all the player's pits are empty + || ComputerPits.All(b => b == 0); // or if all the computer's pits are empty + + public GameState State { get; private set; } + + public void Reset() + { + State = GameState.PlayerMove; + + Array.Fill(_beans, _initialPitValue); + _beans[_playerHome] = 0; + _beans[_computerHome] = 0; + + _moveCount = 0; + _notWonGameMoves[^1] = 0; + } + + public bool IsLegalPlayerMove(int move) => + move is > 0 and < 7 + && _beans[move - 1] > 0; // arrays are zero-based, but moves are one-based + + public void PlayerMove(int move) => MoveAndRegister(move - 1, _playerHome); + + public List ComputerTurn() + { + // keep a list of moves made by the computer in a single turn (1 or 2) + List moves = new(); + + moves.Add(ComputerMove()); // ComputerMove() returns the move made + + // only if a second move is possible, do it + if (State == GameState.ComputerSecondMove) + moves.Add(ComputerMove()); + + return moves; + } + + public GameOutcome GetOutcome() + { + if (State != GameState.Done) + throw new InvalidOperationException("Game is not yet done."); + + int difference = _beans[_playerHome] - _beans[_computerHome]; + var winner = difference switch + { + < 0 => GameWinner.Computer, + 0 => GameWinner.Draw, + > 0 => GameWinner.Player, + }; + + return new GameOutcome(winner, Math.Abs(difference)); + } + + private void MoveAndRegister(int pit, int homePosition) + { + int lastMovedBean = Move(_beans, pit, homePosition); + + // encode moves by player and computer into a 'base 6' number + // e.g. if the player moves 5, the computer moves 2, and the player moves 4, + // that would be encoded as ((5 * 6) * 6) + (2 * 6) + 4 = 196 + if (pit > 6) pit -= 7; + _moveCount++; + if (_moveCount < 9) + _notWonGameMoves[^1] = _notWonGameMoves[^1] * 6 + pit; + + // determine next state based on current state, whether the game's done, and whether the last moved bean moved + // into the player's home position + State = (State, IsDone, lastMovedBean == homePosition) switch + { + (_, true, _) => GameState.Done, + (GameState.PlayerMove, _, true) => GameState.PlayerSecondMove, + (GameState.PlayerMove, _, false) => GameState.ComputerMove, + (GameState.PlayerSecondMove, _, _) => GameState.ComputerMove, + (GameState.ComputerMove, _, true) => GameState.ComputerSecondMove, + (GameState.ComputerMove, _, false) => GameState.PlayerMove, + (GameState.ComputerSecondMove, _, _) => GameState.PlayerMove, + _ => throw new InvalidOperationException("Unexpected game state"), + }; + + // do some bookkeeping if the game is done, but not won by the computer + if (State == GameState.Done + && _beans[_playerHome] >= _beans[_computerHome]) + // add an entry for the next game + _notWonGameMoves.Add(0); + } + + private static int Move(int[] beans, int pit, int homePosition) + { + int beansToMove = beans[pit]; + beans[pit] = 0; + + // add the beans that were in the pit to other pits, moving clockwise around the board + for (; beansToMove >= 1; beansToMove--) + { + // wrap around if pit exceeds 13 + pit = (pit + 1) % 14; + + beans[pit]++; + } + + if (beans[pit] == 1 // if the last bean was sown in an empty pit + && pit is not _playerHome and not _computerHome // which is not either player's home + && beans[12 - pit] != 0) // and the pit opposite is not empty + { + // move the last pit sown and the _beans in the pit opposite to the player's home + beans[homePosition] = beans[homePosition] + beans[12 - pit] + 1; + beans[pit] = 0; + beans[12 - pit] = 0; + } + + return pit; + } + + private int ComputerMove() + { + int move = DetermineComputerMove(); + MoveAndRegister(move, homePosition: _computerHome); + + // the result is only used to return it to the application, so translate it from an array index (between 7 and + // 12) to a pit number (between 1 and 6) + return move - 6; + } + + private int DetermineComputerMove() + { + int bestScore = -99; + int move = 0; + + // for each of the computer's possible moves, simulate them to calculate a score and pick the best one + for (int j = 7; j < 13; j++) + { + if (_beans[j] <= 0) + continue; + + int score = SimulateMove(j); + + if (score >= bestScore) + { + move = j; + bestScore = score; + } + } + + return move; + } + + private int SimulateMove(int move) + { + // make a copy of the current state, so we can safely mess with it + var hypotheticalBeans = new int[14]; + _beans.CopyTo(hypotheticalBeans, 0); + + // simulate the move in our copy + Move(hypotheticalBeans, move, homePosition: _computerHome); + + // determine the 'best' move the player could make after this (best for them, not for the computer) + int score = ScoreBestNextPlayerMove(hypotheticalBeans); + + // score this move by calculating how far ahead we would be after the move, and subtracting the player's next + // move score + score = hypotheticalBeans[_computerHome] - hypotheticalBeans[_playerHome] - score; + + // have we seen the current set of moves before in a drawn/lost game? after 8 moves it's unlikely we'll find any + // matches, since games will have diverged. also we don't have space to store that many moves. + if (_moveCount < 8) + { + int translatedMove = move - 7; // translate from 7 through 12 to 0 through 5 + + // if the first two moves in this game were 1 and 2, and this hypothetical third move would be a 3, + // movesSoFar would be (1 * 36) + (2 * 6) + 3 = 51 + int movesSoFar = _notWonGameMoves[^1] * 6 + translatedMove; + + // since we store moves as a 'base 6' number, we need to divide stored moves by a power of 6 + // let's say we've a stored lost game where the moves were, in succession, 1 through 8, the value stored + // would be: + // 8 + (7 * 6) + (6 * 36) + (5 * 216) + (4 * 1296) + (3 * 7776) + (2 * 46656) + (1 * 279936) = 403106 + // to figure out the first three moves, we'd need to divide by 7776, resulting in 51.839... + double divisor = Math.Pow(6.0, 7 - _moveCount); + + foreach (int previousGameMoves in _notWonGameMoves) + // if this combination of moves so far ultimately resulted in a draw/loss, give it a lower score + // note that this can happen multiple times + if (movesSoFar == (int) (previousGameMoves / divisor + 0.1)) + score -= 2; + } + + return score; + } + + private static int ScoreBestNextPlayerMove(int[] hypotheticalBeans) + { + int bestScore = 0; + + for (int i = 0; i < 6; i++) + { + if (hypotheticalBeans[i] <= 0) + continue; + + int score = ScoreNextPlayerMove(hypotheticalBeans, i); + + if (score > bestScore) + bestScore = score; + } + + return bestScore; + } + + private static int ScoreNextPlayerMove(int[] hypotheticalBeans, int move) + { + // figure out where the last bean will land + int target = hypotheticalBeans[move] + move; + int score = 0; + + // if it wraps around, that means the player is adding to his own pits, which is good + if (target > 13) + { + // prevent overrunning the number of pits we have + target %= 14; + score = 1; + } + + // if the player's move ends up in an empty pit, add the value of the pit on the opposite side to the score + if (hypotheticalBeans[target] == 0 && target is not _playerHome and not _computerHome) + score += hypotheticalBeans[12 - target]; + + return score; + } + + private const int _playerHome = 6; + private const int _computerHome = 13; + private const int _initialPitValue = 3; + + private readonly int[] _beans = new int[14]; + private readonly List _notWonGameMoves = new() { 0 }; // not won means draw or lose + private int _moveCount; +} + +public enum GameState +{ + PlayerMove, + PlayerSecondMove, + ComputerMove, + ComputerSecondMove, + Done, +} + +public enum GameWinner +{ + Player, + Computer, + Draw, +} + +public record struct GameOutcome(GameWinner Winner, int Difference); \ No newline at end of file diff --git a/04_Awari/csharp/Program.cs b/04_Awari/csharp/Program.cs new file mode 100644 index 00000000..750787e7 --- /dev/null +++ b/04_Awari/csharp/Program.cs @@ -0,0 +1,98 @@ +using Awari; + +Console.WriteLine(Tab(34) + "AWARI"); +Console.WriteLine(Tab(15) + "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + +Game game = new(); + +while (true) +{ + game.Reset(); + DisplayGame(); + + while (game.State != GameState.Done) + { + switch (game.State) + { + case GameState.PlayerMove: + PlayerMove(second: false); + break; + case GameState.PlayerSecondMove: + PlayerMove(second: true); + break; + case GameState.ComputerMove: + ComputerTurn(); + break; + } + + DisplayGame(); + } + + var outcome = game.GetOutcome(); + + string outcomeLabel = + outcome.Winner switch + { + GameWinner.Computer => $"I WIN BY {outcome.Difference} POINTS", + GameWinner.Draw => "DRAWN GAME", + GameWinner.Player => $"YOU WIN BY {outcome.Difference} POINTS", + _ => throw new InvalidOperationException($"Unexpected winner {outcome.Winner}."), + }; + Console.WriteLine(outcomeLabel); + Console.WriteLine(); +} + +void DisplayGame() +{ + // display the computer's pits + Console.Write(" "); + foreach (var pit in game.ComputerPits.Reverse()) + Console.Write($"{pit,2} "); + Console.WriteLine(); + + // display both homes + Console.WriteLine($"{game.ComputerHome,2}{Tab(19)}{game.PlayerHome,2}"); + + // display the player's pits + Console.Write(" "); + foreach (var pit in game.PlayerPits) + Console.Write($"{pit,2} "); + Console.WriteLine(); + + Console.WriteLine(); +} + +void PlayerMove(bool second = false) +{ + int move = GetMove(second); + game.PlayerMove(move); +} + +int GetMove(bool second) +{ + string prompt = second ? "AGAIN? " : "YOUR MOVE? "; + + while (true) + { + Console.Write(prompt); + + string input = Console.ReadLine() ?? ""; + + // input must be a number between 1 and 6, and the pit must have > 0 beans + if (int.TryParse(input, out int move) + && game.IsLegalPlayerMove(move)) + return move; + + Console.WriteLine("ILLEGAL MOVE"); + } +} + +void ComputerTurn() +{ + var moves = game.ComputerTurn(); + string movesString = string.Join(",", moves); + + Console.WriteLine($"MY MOVE IS {movesString}"); +} + +string Tab(int n) => new(' ', n); \ No newline at end of file diff --git a/04_Awari/csharp/csharp.csproj b/04_Awari/csharp/csharp.csproj new file mode 100644 index 00000000..6bf2a525 --- /dev/null +++ b/04_Awari/csharp/csharp.csproj @@ -0,0 +1,11 @@ + + + + Exe + net6.0 + enable + enable + Awari + + +