Пример работы с сессией: игра "Крестики-Нолики"

Работу с сессиями в PHP можно проиллюстрировать на несложном примере, например на игре "Крестики-нолики". В игре, исходный код которой приведён ниже, двум игрокам предлагается по очереди делать ходы, ставя на поле крестики и нолики. Выигрывает тот, кто первым наберёт 5 крестиков или ноликов в ряд.

Исходный код игры разделён на два файла: classes.php содержит объявление класса игры TicTacGame. Этот класс содержит всю игровую логику. Второй файл - index.php, который осуществляет взаимодействие с пользователем. Именно к index.php браузер пользователя будет отправлять запросы, и здесь же отображается игровое поле и выводится состояние игры:

Исходный код игры "Крестики-нолики"

Вы можете скачать исходный код примера в архиве, посмотреть текущую версию в репозитории Subversion, или взять текущую версию из репозитория:

svn co http://team.labaka.ru/svn/labaka/tictactoe

classes.php:

<?php

/**
 * Реализация игры "крестики-нолики" на поле произвольного размера.
 * Вариант "ничья" никак не обрабатывается.
 */
class TicTacGame {
    
    /*
     * Размер игрового поля
     */
    private $fieldWidth = 20;
    private $fieldHeight = 20;
    
    /**
     * @var число крестиков или ноликов в ряд для победы.
     */
    private $countToWin = 5;
    
    /**
     * @var массив сделанных ходов вида $field[$x][$y] = $player;
     */
    private $field = array();
    
    /**
     * @var $winnerCells аналогичен $field, но хранит только клетки, которые
     * надо выделить при отображении победившей комбинации.
     */
    private $winnerCells = array();
    
    private $currentPlayer = 1; // 1 или 2, а после окончания игры - null.
    private $winner = null; // после окончания игры будет содержать 1 или 2.
    
    /**
     * Обрабатывает очередной ход. Ставит в указанные координаты на поле
     * символ текущего игрока. Передаёт ход другому игроку, а в случае победы
     * опреляет победителя.
     *
     * Это единственная функция, которая может менять состояние игры.
     */
    public function makeMove($x, $y) {
        // Учитываем ход, если выполняются все условия:
        // 1) игра ещё идет,
        // 2) клетка находится в пределах игрового поля.
        // 3) в поле на указанном месте ещё пусто,
        if(
                $this->currentPlayer
                &&
                $x >= 0 && $x < $this->fieldWidth
                &&
                $y >= 0 && $y < $this->fieldHeight
                &&
                empty($this->field[$x][$y]))
        {
                $current = $this->currentPlayer;

                $this->field[$x][$y] = $current;
                $this->currentPlayer = ($current == 1) ? 2 : 1;
                
                $this->checkWinner();
        }
    }
    
    /**
     * Делает поиск выигравшей комбинации, проходя по всему полю и проверяя
     * 4 направления (горизонталь, вертикаль и 2 диагонали).
     */
    private function checkWinner() {
        for($y = 0; $y < $this->fieldHeight; $y++) {
            for($x = 0; $x < $this->fieldWidth; $x++) {
                $this->checkLine($x, $y, 1, 0);
                $this->checkLine($x, $y, 1, 1);
                $this->checkLine($x, $y, 0, 1);
                $this->checkLine($x, $y, -1, 1);
            }
        }
        if($this->winner) {
            $this->currentPlayer = null;
        }
    }
    
    /**
     * Проверяет, а не находится ли в этом месте поля выигрышная комбинация
     * из необходимого числа крестиков или ноликов.
     * Если выигрышная комбинация найдена, запоминает победителя
     * и саму выигрышную комбинацию в массиве winnerCells.
     *
     * @param $startX начальная точка, от которой проверять наличие комбинации
     * @param $startY
     * @param $dx направление, в котором искать комбинацию
     * @param $dy
     */
    private function checkLine($startX, $startY, $dx, $dy) {
        $x = $startX;
        $y = $startY;
        $field = $this->field;
        $value = isset($field[$x][$y])? $field[$x][$y]: null;
        $cells = array();
        $foundWinner = false;
        if($value) {
            $cells[] = array($x, $y);
            $foundWinner = true;
            for($i=1; $i < $this->countToWin; $i++) {
                $x += $dx;
                $y += $dy;
                $value2 = isset($field[$x][$y])? $field[$x][$y]: null;
                if($value2 == $value) {
                    $cells[] = array($x, $y);
                } else {
                    $foundWinner = false;
                    break;
                }
            }
        }
        if($foundWinner) {
            foreach($cells as $cell) {
                $this->winnerCells[$cell[0]][$cell[1]] = $value;
            }
            $this->winner = $value;
        }
    }

    /*
     * Функции ниже позволяют получить текущее состояние игры и игрового поля.
     */
    
    public function getCurrentPlayer() { return $this->currentPlayer; }
    public function getWinner()        { return $this->winner; }
    public function getField()         { return $this->field; }
    public function getWinnerCells()   { return $this->winnerCells; }
    public function getFieldWidth()    { return $this->fieldWidth; }
    public function getFieldHeight()   { return $this->fieldHeight; }
}

index.php:

<?php
// Подключаем объявление класса игры.
require_once(dirname(__FILE__) . '/classes.php');

session_start();

// Получаем из сессии текущую игру.
// Если игры еще нет, создаём новую.
$game = isset($_SESSION['game'])? $_SESSION['game']: null;
if(!$game || !is_object($game)) {
    $game = new TicTacGame();
}

// Обрабатываем запрос пользователя, выполняя нужное действие.
$params = $_GET + $_POST;
if(isset($params['action'])) {
    $action = $params['action'];
    
    if($action == 'move') {
        // Обрабатываем ход пользователя.
        $game->makeMove((int)$params['x'], (int)$params['y']);
        
    } else if($action == 'newGame') {
        // Пользователь решил начать новую игру.
        $game = new TicTacGame();
    }
}
// Добавляем вновь созданную игру в сессию.
$_SESSION['game'] = $game;


// Отображаем текущее состояние игры в виде HTML страницы.
$width = $game->getFieldWidth();
$height = $game->getFieldHeight();
$field = $game->getField();
$winnerCells = $game->getWinnerCells();

?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN"
  "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" version="XHTML+RDFa 1.0" dir="ltr">
<head profile="http://www.w3.org/1999/xhtml/vocab">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>

<body>
    
<!-- Отображаем состояние игры и игровое поле. -->

<!-- CSS-стили, задающие внешний вид элементов HTML. -->
<style type="text/css">
    .ticTacField {overflow:hidden;}
    .ticTacRow {clear:both;}
    .ticTacCell {float:left; border: 1px solid #ccc; width: 20px; height:20px;
                position:relative; text-align:center;}
    .ticTacCell a {position:absolute; left:0;top:0;right:0;bottom:0}
    .ticTacCell a:hover { background: #aaa; }
    .ticTacCell.winner { background:#f00;}
    
    .icon { display:inline-block; }
    .player1:after { content: 'X'; }
    .player2:after { content: 'O'; }
</style>

<?php if($game->getCurrentPlayer()) { ?>
    <!-- Отображаем приглашение сделать ход. -->
    Ход делает игрок
    <div class="icon player<?php echo $game->getCurrentPlayer() ?>"></div>...
<?php } ?>

<?php if($game->getWinner()) { ?>
    <!-- Отображаем сообщение о победителе -->
    Победил игрок
    <div class="icon player<?php echo $game->getWinner() ?>"></div>!
<?php } ?>

<!-- Рисуем игровое поле, отображая сделанные ходы
и подсвечивая победившую комбинацию. -->    
<div class="ticTacField">
    <?php for($y=0; $y < $height; $y++) { ?>
        <div class="ticTacRow">
            <?php for($x=0; $x < $width; $x++) {
                // $player - игрок, сходивший в эту клетку :), или null, если клетка свободна.
                // $winner - флаг, означающий, что эта клетка должна быть подсвечена при победе.
                $player = isset($field[$x][$y])? $field[$x][$y]: null;
                $winner = isset($winnerCells[$x][$y]);
                $class = ($player? ' player' . $player: '') . ($winner? ' winner': '');
                ?>
                <div class="ticTacCell<?php echo $class ?>">
                    <?php if(!$player) { ?>
                        <!-- Клетка свободна. Отображаем здесь ссылку,
                        на которую нужно кликнуть для совершения хода. -->
                        <a href="?action=move&amp;x=<?php echo $x ?>&amp;y=<?php echo $y ?>"></a>
                    <?php } ?>
                </div>
            <?php } ?>
        </div>
    <?php } ?>
</div>
            
<br/><a href="?action=newGame">Начать новую игру</a>
            
</body>
</html>

Ограничения нашей игры

Эта реализация игры, исходный код которой вы видите выше, работает только в одном браузере. То есть, когда в игру зайдет пользователь с другого браузера или компьютера, для него на сервере будет создана своя отдельная сессия со своим экземпляром игры.Таким образом, играть можно только сидя вдвоём за одним компьютером. В этом смысле игру можно назвать "однопользовательской", поскольку разные пользователи, зашедшие на сайт, взаимодействуют не друг с другом, а только лишь со своей отдельной сесссией и данными в ней.

Чтобы сделать по-настоящему многопользовательскую игру, необходимо предусмотреть механизм взаимодействия пользователей друг с другом. Этот механизм реализуется не на основе сессий (ведь сессия хранит только данные для одного пользователя), а с использованием, например, базы данных, файлов или другого способа хранения данных, при котором разные пользователи могут получить доступ к общим данным игры.

Другой способ реализации

Эта реализация игры "Крестики-нолики" разработана, в основном, с целью ознакомления с механизмами PHP. В нашей игре информация о всех действиях пользователя передаётся на сервер, который эти действия обрабатывает. Но, поскольку мы уже знаем, что по сути такая реализация является однопользовательской, то ту же самую игру можно было бы реализовать иначе: например на JavaScript, выполняющемся только в браузере пользователя. Алгоритм игры прост настолько, что реализация на JavaScript без труда может выполняться в браузере. В этом случае отсутствовал бы расход мощности сервера на постоянные запросы от пользователей, а также расход времени на осуществление этих запросов, ведь вся обработка действий пользователя происходила бы локально, в браузере.

Модель-Вид-Контроллер

Несложно заметить, что исходный код нашей игры разделён по смыслу на несколько условных частей: класс игры TicTacGame, код-обработчик действий пользователя, а также код отображения состояния игры.

Такое разделение ответственности между частями приложения называется "архитектура Модель-Вид-Контроллер" (Model-View-Controller, MVC). Модель - это объект, над которым выполняются некоторые действия. Вид - это код (это может быть класс или даже несколько классов), отображающий состояние модели. Контроллер - код или класс, взаимодействующий с моделью и изменяющий её состояние в соответствии с командами пользователя.

В нашем примере "моделью" является экземпляр класса TicTacGame. TicTacGame содержит полное описание состояния игры: поставленные крестики и нолики, номер текущего игрока, а также информацию о том, кто победил в игре.

"Контроллер" в нашей игре - это фрагмент кода в начале файла index.php, где проверяется, какое действие вызвал пользователь. Контроллер воздействует на модель, вызывая следующий ход в игре, или создавая новую игру.

"Вид" - это код в конце index.php, отображающий HTML-страницу с состоянием игры. Здесь текущее состояние модели отображается путём вывода текстовых сообщений о состоянии, а также рисуется игровое поле с поставленными на него крестиками-ноликами.

В программе, оперирующей различными типами объектов ("моделей"), могут присутствовать различные реализации контроллера и вида для каждого из типов объектов, причём здесь могут раскрыться все преимущества объектно-ориентированного подхода, когда классы вида или контроллера наследуются друг от друга.

Разделение кода на смысловые фрагменты Model-View-Controller, даже если программа совсем маленькая, как в нашем примере, значительно улучшает читаемость кода. Программа с такой организацией представляет собой не "лапшу", а чётко структурированный код, где обработкой ввода пользователя, обработкой данных и их отображением занимаются специально предназначенные для этого классы или фрагменты кода.

CSS

Пытливый читатель, недавно начавший знакомиться с PHP, вероятно, заметил блок текста в index.php, обрамлённый тегами <style>...</style>, назначение которого неясно. Здесь объявляется стиль CSS (Cascading Style Sheet) для страницы, которую мы отображаем: перечислены правила, в соответствии с которыми некоторым элементам в HTML-документе назначаются свойства отображения. Эти свойства могут быть самыми разными: цвет текста, фона, стиль рамки, параметры расположения на странице и так далее.

Благодаря стилям CSS достигается ещё один уровень разделения ответственности: код, формирующий структуру HTML-документа, может не знать о том, как должны выглядеть элементы документа, ведь эта информация содержится в CSS. И наоборот, один и тот же фрагмент HTML может быть оформлен по-разному, достаточно лишь поменять стили в CSS.

Возможности СSS - тема достаточно интересная, но парой абзацев её не охватить, поэтому не будем углубляться в неё сейчас, а лучше осветим в отдельной главе.

Раздел: