diff --git a/.github/workflows/csharp.yaml b/.github/workflows/csharp.yaml index 29ab41e..cb63ee3 100644 --- a/.github/workflows/csharp.yaml +++ b/.github/workflows/csharp.yaml @@ -20,3 +20,5 @@ jobs: uses: actions/setup-dotnet@v4 - name: Build run: dotnet build tictactoe_csharp.csproj + - name: Run + run: dotnet run -- -X 4 -O 4 diff --git a/tictactoe_csharp/tictactoe.cs b/tictactoe_csharp/tictactoe.cs index 6a294ae..110184a 100644 --- a/tictactoe_csharp/tictactoe.cs +++ b/tictactoe_csharp/tictactoe.cs @@ -1,37 +1,88 @@ -Game game = new Game(); -game.playGame(); +AIStrength X = 0; // Variable to store the value after -X +AIStrength O = 0; // Variable to store the value after -O -class Game + +// Iterate through the arguments +for (int i = 0; i < args.Length; i++) { + // Check if the argument starts with '-' + if (args[i].StartsWith('-')) + { + // Check for -X argument + if (args[i] == "-X" && i + 1 < args.Length) + { + // Parse the value after -X + if (Enum.TryParse(args[i + 1], out AIStrength value)) + { + X = value; + } + } + // Check for -O argument + else if (args[i] == "-O" && i + 1 < args.Length) + { + // Parse the value after -O + if (Enum.TryParse(args[i + 1], out AIStrength value)) + { + O = value; + } + } + } +} + +Game game = new(X, O); +game.PlayGame(); - private char[] board; - private int[][] win_conditions = [ +enum AIStrength +{ + HUMAN = 0, + EASY = 1, + MEDIUM = 2, + HARD = 3, + IMPOSSIBLE = 4 +} + +enum Score { WIN = 1, TIE = 0, LOSE = -1 } + +struct Move(int spot, Score score) +{ + public int spot = spot; + public Score score = score; +} + +class Game(AIStrength X = 0, AIStrength O = 0) +{ + private readonly char[] board = ['0', '1', '2', '3', '4', '5', '6', '7', '8']; + private readonly AIStrength X_strength = X; + private readonly AIStrength O_strength = O; + + private readonly int[][] win_conditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns [0, 4, 8], [2, 4, 6] // diagonals ]; - public Game() + private void PlayerTurn(char player) { - board = ['0', '1', '2', '3', '4', '5', '6', '7', '8']; - } - private void player_turn(char player) { int move; - while (true) { + while (true) + { Console.WriteLine($"Player {player}, enter your move (0-8): "); - show_board(); - if (!int.TryParse(Console.ReadLine(), out move)) { + ShowBoard(); + if (!int.TryParse(Console.ReadLine(), out move)) + { Console.WriteLine("Invalid move. Enter a number."); continue; } - if (move < 0 || move > 8) { + if (move < 0 || move > 8) + { Console.WriteLine("Invalid move. Enter a number in bounds."); continue; } - if (board[move] == 'X' || board[move] == 'O') { + if (board[move] == 'X' || board[move] == 'O') + { Console.WriteLine("Invalid move. That space is already taken."); continue; } @@ -40,24 +91,139 @@ private void player_turn(char player) { board[move] = player; } - private bool is_winner(char player) { - foreach (var condition in win_conditions) { - if (condition.All(i => board[i] == player)) { + private int[] EmptyCells() + { + return board.Select((c, i) => c != 'X' && c != 'O' ? i : -1).Where(i => i != -1).ToArray(); + } + + private int RandomMove() + { + var emptyCells = EmptyCells(); + return emptyCells[new Random().Next(emptyCells.Length)]; + } + + private int? TryWinningMove(char player) + { + foreach (var condition in win_conditions) + { + var emptyCells = condition.Where(i => board[i] != 'X' && board[i] != 'O').ToArray(); + if (emptyCells.Length == 1 && condition.Count(i => board[i] == player) == 2) + { + return emptyCells[0]; + } + } + return null; + } + + private int WinningMove(char player) + { + var move = TryWinningMove(player); + return move ?? RandomMove(); + } + + private int WinningBlockingMove(char player) + { + var move = TryWinningMove(player); + if (move != null) + { + return move.Value; + } + var opponent = SwapPlayer(player); + move = TryWinningMove(opponent); + return move ?? RandomMove(); + } + + private static Score NegateScore(Score score) + { + return score switch + { + Score.WIN => Score.LOSE, + Score.LOSE => Score.WIN, + _ => score + }; + } + + private Move BestMove(char player) + { + var bestMove = new Move(-1, Score.LOSE); + if (IsWinner(player)) + { + return new Move(-1, Score.WIN); + } + if (IsWinner(SwapPlayer(player))) + { + return new Move(-1, Score.LOSE); + } + + var emptyCells = EmptyCells(); + if (emptyCells.Length == 0) + { + return new Move(-1, Score.TIE); + } + if (emptyCells.Length == 9) + { + return new Move(RandomMove(), Score.TIE); + } + + foreach (var cell in emptyCells) + { + board[cell] = player; + var score = NegateScore(BestMove(SwapPlayer(player)).score); + board[cell] = (char)(cell + '0'); + if (score >= bestMove.score) + { + bestMove = new Move(cell, score); + } + } + return bestMove; + } + + private int MinmaxMove(char player) + { + return BestMove(player).spot; + } + + private void AiTurn(char player, AIStrength aIStrength) + { + Console.WriteLine($"AI turn as player {player} with strength {aIStrength}"); + ShowBoard(); + var move = aIStrength switch + { + AIStrength.HUMAN => throw new Exception("AIStrength.HUMAN should not be used for AI turn."), + AIStrength.EASY => RandomMove(), + AIStrength.MEDIUM => WinningMove(player), + AIStrength.HARD => WinningBlockingMove(player), + AIStrength.IMPOSSIBLE => MinmaxMove(player), + _ => throw new ArgumentException("Invalid AI strength") + }; + board[move] = player; + Thread.Sleep(1000); + } + + private bool IsWinner(char player) + { + foreach (var condition in win_conditions) + { + if (condition.All(i => board[i] == player)) + { return true; } } return false; } - private bool is_board_full() { + private bool IsBoardFull() + { return board.All(c => c == 'X' || c == 'O'); } - private char swapPlayer(char player) { + private static char SwapPlayer(char player) + { return player == 'X' ? 'O' : 'X'; } - private void show_board() { + private void ShowBoard() + { Console.WriteLine($"{board[0]} | {board[1]} | {board[2]}"); Console.WriteLine("---------"); Console.WriteLine($"{board[3]} | {board[4]} | {board[5]}"); @@ -65,21 +231,36 @@ private void show_board() { Console.WriteLine($"{board[6]} | {board[7]} | {board[8]}"); } - public void playGame() + public void PlayGame() { char currentPlayer = 'X'; - while (true) { - player_turn(currentPlayer); - if (is_winner(currentPlayer)) { + while (true) + { + if (currentPlayer == 'X' && X_strength != AIStrength.HUMAN) + { + AiTurn(currentPlayer, X_strength); + } + else if (currentPlayer == 'O' && O_strength != AIStrength.HUMAN) + { + AiTurn(currentPlayer, O_strength); + } + else + { + PlayerTurn(currentPlayer); + } + + if (IsWinner(currentPlayer)) + { Console.WriteLine($"Player {currentPlayer} wins!"); break; } - if (is_board_full()) { + if (IsBoardFull()) + { Console.WriteLine("It's a tie!"); break; } - currentPlayer = swapPlayer(currentPlayer); + currentPlayer = SwapPlayer(currentPlayer); } - show_board(); + ShowBoard(); } } diff --git a/tictactoe_csharp/tictactoe_csharp.csproj b/tictactoe_csharp/tictactoe_csharp.csproj index 2150e37..e7bb38a 100644 --- a/tictactoe_csharp/tictactoe_csharp.csproj +++ b/tictactoe_csharp/tictactoe_csharp.csproj @@ -5,6 +5,9 @@ net8.0 enable enable + 4 + preview + true