CLI приложение Крестики-Нолики

+9
446

Расширение своего опыта всегда положительно влияет на решение обычных задач. PHP изначально разрабатывался как язык для создания веб-сайтов, но существует и официальная поддержка режима CLI. По моему скромному опыту, могу сказать, что именно работа с PHP в командной строке дает возможность по-настоящему почувствовать язык. В качестве иллюстрации я написал cli-приложение, которое реализует некогда любимую игру всей детворы Крестики-Нолики.

Подготовка

Понятно, что на вашем компьютере должен быть установлен PHP. Отговорки вроде: Ой, да я использую OpenServer, не принимаются. Находите в меню Терминал, щелкаете и работаете. 
Создайте, где вам удобно директорию xo. Пусть полный путь к этой директории будет, например, /home/vasya/php-apps/xo. Далее в консоли(терминале)

  1. # переходите в эту директорию
  2. cd /home/vasya/php-apps/xo
  3. # проверяете версию PHP
  4. php -v


Я разрабатывал на PHP 7.4, думаю, на  PHP 7.0 и новее все будет работать. 
Без Hello, World! лучше даже и не начинать, поэтому создайте /home/vasya/php-apps/xo/hw.php со следующим содержимым

  1. <?php
  2. echo "Hello, World!";


Далее в консоли(терминале)

  1. php -f hw.php


Если вы видите вывод Hello, World!, то двигаемся дальше.

План приложения

Создаете /home/vasya/php-apps/xo/xo.php

  1. <?php
  2. class CrossZero {
  3.  
  4. private $size; // размер таблицы
  5. private $table; // сама таблица
  6. public const EMPTY = "-"; // символ пустой клетки
  7. public const X = "x"; // крестик
  8. public const O = "o"; // нолик
  9.  
  10. public function __construct(int $size = 3) {
  11. $this->size = $size;
  12. $this->initTable();
  13. }
  14.  
  15. private function initTable(): void {
  16.  
  17. for ($i = 0; $i < $this->size; $i++) {
  18. for ($j = 0; $j < $this->size; $j++) {
  19. $this->table[$i][$j] = self::EMPTY;
  20. }
  21. }
  22.  
  23. }
  24. // далее сюда следует поместить методы для работы с таблицей
  25.  
  26. }
  27.  
  28. class Helper {
  29. // здесь будет метод, реализующий ввод с клавиатуры
  30. }
  31.  
  32. // последовательность работы приложения
  33.  
  34. $ts = 3; // table size - размер таблицы
  35. $xo = new CrossZero($ts);
  36.  
  37. while (true) {
  38. // ход человека, координаты х вводятся с клавиатуры
  39.  
  40. // человек выиграл? если да, то цикл прерывается
  41.  
  42. // таблица заполнена полностью? если да, то цикл прерывается
  43.  
  44. // ход компьютера, координаты о генерируются случайным образом
  45.  
  46. // компьютер выиграл? если да, то цикл прерывается
  47.  
  48. // таблица заполнена полностью? если да, то цикл прерывается
  49.  
  50. }

Четыре замечания:
— все помещено в один файл;
— константы можно использовать как внутри класса, например так, self::EMPTY, так и вне класса без создания экземпляра класса, например так, CrossZero::EMPTY;
— то, что не используется вне класса (свойства, методы) имеет область видимости private;
— таблица $table имеет область видимости private, это личное предпочтение, но и с точки зрения ООП, это правильнее.

Элементы логики работы(класс CrossZero)

Игра может закончится по 2-м причинам:
— таблица заполнена полностью,
— человек или компьютер одержал победу.

С реализацией заполнения все просто, нужно пробежаться по таблице и посмотреть, не осталось ли пустых ячеек.

  1. public function tableIsFull(): bool {
  2.  
  3. for ($i = 0; $i < $this->size; $i++) {
  4. for ($j = 0; $j < $this->size; $j++) {
  5. if ($this->table[$i][$j] === self::EMPTY) return false;
  6. }
  7. }
  8.  
  9. return true;
  10. }

А вот, чтобы проверить факт победы, нужно проверить каждую строку, столбец, диагональ на заполнение их self::X или self::O.

  1. private function checkRows(string $simbol): bool {
  2.  
  3. for ($row = 0; $row < $this->size; $row++) {
  4. $count = 0;
  5.  
  6. for ($col = 0; $col < $this->size; $col++) {
  7. if ($this->table[$row][$col] == $simbol) $count++;
  8. }
  9.  
  10. if ($count == $this->size) return true;
  11. }
  12.  
  13. return false;
  14. }
  15.  
  16. private function checkColumns(string $simbol): bool {
  17.  
  18. for ($col = 0; $col < $this->size; $col++) {
  19. $count = 0;
  20.  
  21. for ($row = 0; $row < $this->size; $row++) {
  22. if ($this->table[$row][$col] == $simbol) $count++;
  23. }
  24.  
  25. if ($count == $this->size) return true;
  26. }
  27.  
  28. return false;
  29. }
  30.  
  31. private function checkDiagonal(string $simbol): bool {
  32. $count = 0;
  33.  
  34. for ($i = 0; $i < $this->size; $i++) {
  35. if ($this->table[$i][$i] == $simbol) $count++;
  36. }
  37.  
  38. if ($count == $this->size) return true;
  39.  
  40. return false;
  41. }
  42.  
  43. private function check2Diagonal(string $simbol): bool {
  44. $count = 0;
  45.  
  46. for ($i = 0; $i < $this->size; $i++) {
  47. if ($this->table[$this->size - $i -1][$i] == $simbol) $count++;
  48. }
  49.  
  50. if ($count == $this->size) return true;
  51.  
  52. return false;
  53. }
  54.  
  55. public function checkWin(string $simbol): bool {
  56.  
  57. if ($this->checkRows($simbol) === true) return true;
  58. if ($this->checkColumns($simbol) === true) return true;
  59. if ($this->checkDiagonal($simbol) === true) return true;
  60. if ($this->check2Diagonal($simbol) === true) return true;
  61.  
  62. return false;
  63. }

Чтобы записать нужный символ в приватную таблицу, реализован отдельный метод. Он не просто втыкает символ в указанное место, а проверяет, не занята ли ячейка. В этом случае запись не происходит, а метод возвращает false.

  1. public function setSimbol(int $row, int $col, string $simbol): bool {
  2.  
  3. if ($this->table[$row][$col] != self::EMPTY) return false;
  4.  
  5. $this->table[$row][$col] = $simbol;
  6. return true;
  7. }

Ну, и печать таблицы

  1. public function printTable(): void {
  2.  
  3. for ($i = 0; $i < $this->size; $i++) {
  4. $s = "";
  5. for ($j = 0; $j < $this->size; $j++) {
  6. $s.= sprintf(" %s", $this->table[$i][$j]);
  7. }
  8. $s.= "\n";
  9. echo $s;
  10. }
  11.  
  12. echo "\n";
  13. }

На этом с классом CrossZero покончено.

Ввод с клавиатуры реализован методом класса Helper. Вводимые данные, кстати, проверяются на валидность

  1. public static function readNumber(int $min, int $max): int {
  2.  
  3. while (true) {
  4. $line = trim(fgets(STDIN));
  5.  
  6. if (is_numeric($line) === false) {
  7. echo "Должно быть введено число. Снова :";
  8. continue;
  9. }
  10.  
  11. $number = (int)$line;
  12.  
  13. if ($number < $min || $number > $max) {
  14. echo "Число не принадлежит нужному диапазону. Снова :";
  15. continue;
  16. } else {
  17. break;
  18. }
  19. }
  20.  
  21. return $number;
  22. }

Наконец, реализация цикла while() из последовательности работы приложения

  1. while (true) {
  2. // ход человека
  3. do {
  4. echo "Введите номер строки :";
  5. $row = Helper::readNumber(1, $ts);
  6. echo "Введите номер столбца :";
  7. $col = Helper::readNumber(1, $ts);
  8. } while ($xo->setSimbol($row - 1, $col - 1, CrossZero::X) === false);
  9.  
  10. $xo->printTable();
  11.  
  12. // человек выиграл?
  13. if ($xo->checkWin(CrossZero::X) === true) {
  14. echo "Вы выиграли!\n";
  15. break;
  16. }
  17. // таблица заполнена полностью?
  18. if ($xo->tableIsFull() === true) {
  19. echo "Таблица заполнена полностью.\n";
  20. break;
  21. }
  22.  
  23. // ход компьютера
  24. do {
  25. $row = random_int(0, $ts - 1);
  26. $col = random_int(0, $ts - 1);
  27. } while ($xo->setSimbol($row, $col, CrossZero::O) === false);
  28.  
  29. $xo->printTable();
  30.  
  31. // компьютер выиграл?
  32. if ($xo->checkWin(CrossZero::O) === true) {
  33. echo "Компьютер выиграл!\n";
  34. break;
  35. }
  36. // таблица заполнена полностью?
  37. if ($xo->tableIsFull() === true) {
  38. echo "Таблица заполнена полностью.\n";
  39. break;
  40. }
  41. }

Если вы дочитали до этого места и не потеряли нить повествования, то можете проверить так ли это, собрав из кусочков полное приложение. Запускать из консоли

  1. php -f xo.php

Если не получилось — не расстраивайтесь! Вот мой  xo.php .

В данном приложении реализован минимальный функционал, при желании можно сделать действия компьютера более изощренными. Попробуйте запустить приложение с размером таблицы 4х4, 5х5. Можно устроить, что первыми будут ходить нолики. Играйтесь!

Вместо заключения

Если человек начинает писать на C, Rust, Go, Java, Scala, то он начинает этот процесс именно в консоли. Но в PHP, по крайней мере в русско-говорящем сегменте, как-то больше принято начинать с установки OpenServer, XAMPP… и сразу «брать быка за рога» — писать веб-приложение, называется это обычно «От новичка до профессионала». В результате такого подхода пациента сталкивают сразу с PHP, HTML, CSS, SQL, а особо продвинутые туда еще JavaScript добавляют. Поэтому немудрено, что вместо нескольких полезных навыков он(пациент) получает полную «кашу» в голове. Показано много всего и много действительно полезного, но применить это всё человек не в состоянии.

Но это мой взгляд.  Как учиться вам — вам и виднее.

Полезная ссылка

0
Реклама Реклама #
0
olegan olegan 30 дней назад #

Это нужно на сайте Instant?

+2
IamB IamB 30 дней назад #

Признаться, я ожидал такого вопроса. Типичным cli-приложением в CMS является cron.php. И думаю, что, если посетители этого сайта станут играться в игрушки вроде моей, то тем на форуме, где ТС беспомощно разводит руками и стенает: А у меня cron не работает, не станет.

+1
olegan olegan 30 дней назад #

Спасибо за разъяснение.Жаль я не программист и не могу оценить нужность всего этого.

Используя этот сайт, вы соглашаетесь с тем, что мы используем файлы cookie.