💾 Archived View for code.lanterne.chilliet.eu › src › TicTacToe › RequestHandler.php captured on 2023-09-28 at 15:43:37.

View Raw

More Information

⬅️ Previous capture (2021-12-17)

-=-=-=-=-=-=-

<?php

declare(strict_types=1);

namespace MCMic\Gemini\TicTacToe;

use MCMic\Gemini;

class RequestHandler extends Gemini\RequestHandler
{
    /**
     * @var array<string,?string>
     */
    protected static array $renderedChar = [
        'o' => '◯',
        '-' => null,
        'x' => '⨯',
    ];

    public function handle(Gemini\Request $request): Gemini\Response
    {
        $stateString = substr($request->path ?? '/', 1);
        if ($stateString === '') {
            /* Redirect to new game */
            return new Gemini\Response\Redirect('---------');
        }
        if (preg_match('/^[ox-]{9}$/', $stateString) !== 1) {
            throw new Gemini\Exception('Invalid request "' . $stateString . '"', 59);
        }

        $output = '# ❌⭕ Tic Tac Toe ⭕❌' . "\r\n";
        $output .= "\r\n";

        $legalMoves = [];
        $move = -1;
        while (($move = strpos($stateString, '-', $move + 1)) !== false) {
            $legalMoves[] = $move;
        }
        if (count($legalMoves) === 0) {
            $output .= '## Game has ended' . "\r\n";
            $output .= "\r\n";
        } elseif ($stateString !== '---------') {
            $randomMove = array_splice($legalMoves, random_int(0, (count($legalMoves) - 1)), 1)[0];
        }

        $state = array_chunk(str_split($stateString), 3);

        [$winner, $highlights] = $this->testWinner($state);

        if (($winner === null) && isset($randomMove)) {
            $stateString[$randomMove] = 'o';
            $state = array_chunk(str_split($stateString), 3);
            [$winner, $highlights] = $this->testWinner($state);
        }

        $output .= $this->renderBoard($state, $highlights);
        $output .= "\r\n";


        if ($winner === 'x') {
            $output .= '## You won!' . "\r\n";
            $output .= "\r\n";
            $output .= '=> ' . $request->buildUrl('/---------') . ' Replay' . "\r\n";
        } elseif ($winner === 'o') {
            $output .= '## You lost!' . "\r\n";
            $output .= "\r\n";
            $output .= '=> ' . $request->buildUrl('/---------') . ' Replay' . "\r\n";
        } elseif (count($legalMoves) === 0) {
            $output .= '## Draw!' . "\r\n";
            $output .= "\r\n";
            $output .= '=> ' . $request->buildUrl('/---------') . ' Replay' . "\r\n";
        } else {
            foreach ($legalMoves as $i) {
                $newStateString = $stateString;
                $newStateString[$i] = 'x';
                $output .= '=> ' . $request->buildUrl('/' . $newStateString) . ' ' . chunk_split($newStateString, 3, '/') . ' ' . ($i + 1) . "\r\n";
            }
        }

        $output .= $this->getFooter();

        return new Gemini\Response\Success($output, 'text/gemini', 'en');
    }

    protected function getFooter(): string
    {
        $footer = "\r\n";
        $footer .= '## Information' . "\r\n";
        $footer .= "\r\n";
        $footer .= '* A game by MCMic' . "\r\n";
        $footer .= '* Made with PHP' . "\r\n";
        $footer .= '=> gemini://code.lanterne.chilliet.eu Source code' . "\r\n";
        $footer .= '=> https://framagit.org/MCMic/gemini-games.git Git repository' . "\r\n";
        $footer .= "\r\n";
        return $footer;
    }

    /**
     * @param array<array<string>> $state
     * @param array<string> $highlights
     */
    protected function renderBoard(array $state, array $highlights): string
    {
        $output = '```' . "\r\n";
        $board = new Gemini\UnicodeBoard(3, 3);

        for ($l = 0; $l < 3; $l++) {
            for ($c = 0; $c < 3; $c++) {
                $board->setSquare($l, $c, static::$renderedChar[$state[$l][$c]] ?? mb_chr(0x2080 + ($l * 3 + $c) + 1, 'UTF-8'), in_array("$l$c", $highlights, true));
            }
        }

        $output .= $board->render();
        $output .= '```' . "\r\n";

        return $output;
    }

    /**
     * @param array<array<string>> $state
     */
    protected function isCaseEmpty(array $state, int $pos): bool
    {
        return (array_merge(...$state)[$pos] === '-');
    }

    /**
     * @param array<array<string>> $state
     * @return array{?string, array<string>}
     */
    protected function testWinner(array $state): array
    {
        for ($i = 0; $i < 3; $i++) {
            // Line
            if (
                ($state[$i][0] === $state[$i][1]) &&
                ($state[$i][0] === $state[$i][2]) &&
                ($state[$i][0] !== '-')
            ) {
                return [$state[$i][0], [$i . '0',$i . '1',$i . '2']];
            }
            // Column
            if (
                ($state[0][$i] === $state[1][$i]) &&
                ($state[0][$i] === $state[2][$i]) &&
                ($state[0][$i] !== '-')
            ) {
                return [$state[0][$i], ['0' . $i,'1' . $i,'2' . $i]];
            }
        }
        // Diagonals
        if (
            ($state[0][0] === $state[1][1]) &&
            ($state[0][0] === $state[2][2]) &&
            ($state[0][0] !== '-')
        ) {
            return [$state[0][0], ['00','11','22']];
        }
        if (
            ($state[0][2] === $state[1][1]) &&
            ($state[0][2] === $state[2][0]) &&
            ($state[0][2] !== '-')
        ) {
            return [$state[0][2], ['02','11','20']];
        }
        return [null, []];
    }
}