diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..4ffeff8 --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,33 @@ +# This workflow will do stuff with the php version of tictactoe + +name: php + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./tictactoe_php + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + - name: Lint. + run: | + php -l src/tictactoe.php + - name: PHPUnit tests + uses: php-actions/phpunit@v3 + with: + args: "tests/" + - name: Run + run: | + php tests/runTicTacToe.php -X 4 -O 4 diff --git a/README.md b/README.md index efa5408..c5d513d 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,15 @@ julia --project=. test/runtests.jl julia --project=. test/runexample.jl -X 3 -O 3 ``` +## TicTacToe-PHP + +Version using [PHP](https://www.php.net/). + +To test and run: +```bash +phpunit --testdox tests +php tests/runTicTacToe.php -X 4 -O 4 +``` ## TicTacToe-scratch Very simple two player tictactoe game with Scratch. diff --git a/tictactoe_php/src/tictactoe.php b/tictactoe_php/src/tictactoe.php new file mode 100644 index 0000000..9808386 --- /dev/null +++ b/tictactoe_php/src/tictactoe.php @@ -0,0 +1,233 @@ +index = $index; + $this->score = $score; + } +} + + +function show_board(array $board): void +{ + echo " $board[0] | $board[1] | $board[2] \n"; + echo "---+---+---\n"; + echo " $board[3] | $board[4] | $board[5] \n"; + echo "---+---+---\n"; + echo " $board[6] | $board[7] | $board[8] \n"; +} + +function swap_player(string &$player): string +{ + return $player === "X" ? "O" : "X"; +} + +function flush_stdin(): void +{ + while (fgets(STDIN)) { + } +} + +function player_turn(array &$board, string $player): void +{ + echo "Player $player, enter your move (0-8): \n"; + show_board($board); + $position = -1; + while (true) { + $position = preg_replace('/\s+/', '', fgets(STDIN)); + if (ctype_digit($position)) { + $position = intval($position); + } else { + echo "Invalid input. Please enter a number between 0 and 8!\n"; + continue; + } + if ($position < 0 || $position > 8) { + echo "Input out of range. Please enter a number between 0 and 8!\n"; + continue; + } + if ($board[$position] === "X" || $board[$position] === "O") { + echo "Cell already taken. Please choose another one!\n"; + continue; + } + $board[$position] = $player; + break; + } +} + +function is_winner(array $board, string $player): bool +{ + foreach (WINNING_COMBINATIONS as $combination) { + if ( + $board[$combination[0]] === $player && + $board[$combination[1]] === $player && + $board[$combination[2]] === $player + ) { + return true; + } + } + return false; +} + +function is_board_full(array $board): bool +{ + foreach ($board as &$cell) { + if ($cell !== "X" && $cell !== "O") { + return false; + } + } + return true; +} + +function get_empty_cells(array $board): array +{ + $empty_cells = []; + foreach ($board as $index => $cell) { + if ($cell !== "X" && $cell !== "O") { + $empty_cells[] = $index; + } + } + return $empty_cells; +} + +function random_value(array $array): int +{ + return $array[array_rand($array)]; +} + +function random_move(array $board): int +{ + $empty_cells = get_empty_cells($board); + return random_value($empty_cells); +} + +function get_winning_move(array $board, string $player): ?int +{ + foreach (WINNING_COMBINATIONS as $combination) { + $open_cells = []; + $done_cells = 0; + foreach ($combination as $cell) { + if ($board[$cell] === $player) { + $done_cells++; + } elseif ($board[$cell] !== swap_player($player)) { + $open_cells[] = $cell; + } + } + if ($done_cells === 2 && count($open_cells) === 1) { + return $open_cells[0]; + } + } + return null; +} + +function winning_move(array $board, string $player): int +{ + $move = get_winning_move($board, $player); + return is_null($move) ? random_move($board) : $move; +} + +function winning_or_blocking_move(array $board, string $player): int +{ + $move = get_winning_move($board, $player); + if (!is_null($move)) { + return $move; + } + $opponent = $player === "X" ? "O" : "X"; + $move = get_winning_move($board, $opponent); + return is_null($move) ? random_move($board) : $move; +} + +function get_optimal_move(array $board, string $player): Move +{ + $best_move = new Move(0, -1000); + if (is_winner($board, $player)) { + $best_move->score = 1; + return $best_move; + } + if (is_winner($board, swap_player($player))) { + $best_move->score = -1; + return $best_move; + } + $empty_cells = get_empty_cells($board); + + if (!$empty_cells) { + $best_move->score = 0; + return $best_move; + } + + if (count($empty_cells) === 9) { + $best_move->index = random_value($empty_cells); + return $best_move; + } + + foreach ($empty_cells as $index) { + $board[$index] = $player; + $score = -get_optimal_move($board, swap_player($player))->score; + $board[$index] = strval($index); + if ($score > $best_move->score) { + $best_move->score = $score; + $best_move->index = $index; + } + } + return $best_move; +} + +function minmax(array $board, string $player): int +{ + return get_optimal_move($board, $player)->index; +} + + + +function ai_turn(array &$board, string $player, int $strength): void +{ + echo "AI turn as player $player with strength $strength.\n"; + show_board($board); + $move = match ($strength) { + 1 => random_move($board), + 2 => winning_move($board, $player), + 3 => winning_or_blocking_move($board, $player), + default => minmax($board, $player), + }; + $board[$move] = $player; + sleep(1); +} + +function play_game(?int $X_strength, ?int $O_strength): void +{ + $board = ["0", "1", "2", "3", "4", "5", "6", "7", "8"]; + $player = "X"; + + while (true) { + if ($player === "X" && !is_null($X_strength)) { + ai_turn($board, $player, $X_strength); + } elseif ($player === "O" && !is_null($O_strength)) { + ai_turn($board, $player, $O_strength); + } else { + player_turn($board, $player); + } + if (is_winner($board, $player)) { + echo "Player $player wins!\n"; + break; + } + if (is_board_full($board)) { + echo "It's a draw!\n"; + break; + } + $player = swap_player($player); + } + + show_board($board); +} diff --git a/tictactoe_php/tests/TictactoeTest.php b/tictactoe_php/tests/TictactoeTest.php new file mode 100644 index 0000000..9d38a14 --- /dev/null +++ b/tictactoe_php/tests/TictactoeTest.php @@ -0,0 +1,32 @@ +expectOutputString(" X | O | X \n---+---+---\n O | X | O \n---+---+---\n X | O | X \n"); + show_board($board); + } + + public function testSwapPlayer(): void + { + $player = 'X'; + $this->assertSame('O', swap_player($player)); + } + + public function testMinmax(): void + { + $board = ['X', '1', '2', '3', '4', '5', '6', '7', '8']; + $player = 'O'; + $move = get_optimal_move($board, $player); + $this->assertSame(4, $move->index); + $this->assertSame(0, $move->score); + } +} diff --git a/tictactoe_php/tests/runTicTacToe.php b/tictactoe_php/tests/runTicTacToe.php new file mode 100644 index 0000000..720a924 --- /dev/null +++ b/tictactoe_php/tests/runTicTacToe.php @@ -0,0 +1,21 @@ +