Крестики-Нолики. Оценка (критика) кода
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.
Внимание! Это довольно старый топик, посты в него не попадут в новые, и их никто не увидит. Пишите пост, если хотите просто дополнить топик, а чтобы задать новый вопрос — начните новый.
Здравствуйте.
Изучаю C++ пару месяцев и захотелось для начала написать свою простую игру Крестики-Нолики (пока без GUI и ИИ).
Хотелось бы узнать разного рода мнения о моём коде, вообщем нужна ваша критика :)
обновленный линк на GitHub:
https://github.com/loktionov129/CPP_Tic-Tac-Toe
Класс Board отвечает за внутреннее представление игрового поля и игровую логику, а именно:
+ Геттер и Сеттер для конкретной ячейки поля (по координатам x и y)
+ Определение конца игры (ничья либо же победа одного из игроков)
Класс Game отвечает за визуальную часть:
+ Осуществление хода игроком
+ Отрисовка игровой информации и игрового поля на экран
Почему ДОСКА определяет завершена ли игра?
ДОСКА этого решать не должна.
Правила игры же определяются игрой, а не доской.
На одной и той же доске можно играть как в шахматы, так и в шашки.
И зачем здесь динамическая память?
Просто «потому что могу»?
Зачем оно const, если здесь явно видно,
что оно не должно быть таковым?
Очищение доски означает изменение данных.
Из-за таких вот манипуляций,
пришлось сделать board mutable.
Тоже самое с
В общем, работайте. :)
В дополнение к предыдущему оратору:
0). Плохо организован репозиторий на GitHub. Вместо того, что бы скачать 8.39кБ исходников крестиков-ноликов, пришлось качать все проекты (26МБ) целиком. Что не есть гут ((
1).
setlocale
нормально работает, если исходники сохранены в кодировке windows-1251. У тебя файлы с кириллицей сохранены в UTF-8. В итоге в консоли нечитаемый текст.2). Юзабилити: при игре неудобно «вычислять» координаты следующего хода. Тем более с нумерацией от 0.
3). Ничейную позицию можно определить гораздо раньше, чем вся доска будет заполнена.
4). Алгоритм проверки победителя учитывает только главные диагонали поля. Например при игре на поле 5х5 с условием «3 в ряд» позиция
не считается выигрышем.
5). В классах и методах каша. Надо чётко определить за какие данные и за какие действия отвечает класс
Board
, а за какиеGame
. Логику игры надо перенести вGame
, а отображение доски — вBoard
. Всё ООП разбивается об эту ошибку и превращается в спагетти. Кроме того, при нормальном проектировании классов некоторые методы (из существующих) станут гораздо проще. (Например отпадёт необходимость передаватьthis
как параметр.)6). Каша из
char player
иenum { STATE_NEXT_STEP = -1, STATE_DRAW = 0, STATE_X_WINNER, STATE_O_WINNER}
. (Кстати,enum { ... }
имеет типint
.)7).
#pragma once
и#ifndef BLA_BLA_H ... #endif
— масло масляное. Однако первое «масло» зависит от компилятора. В данном случае это ни на что не влияет, но для чистоты кода#pragma once
лучше убрать.Благодарю за оперативность :)
Здесь я отталкивался от 'Разделения мух и котлет'. Т.е. Board — Model, Game — View.
Если так рассуждать, то да, логично будет перенести определение конца игры в Game из Board.
На данный момент, как я думаю, так у меня самая важная проблема — это архитектура. Можете что-нибудь посоветовать насчёт проектирования ?
Где-то читал что лучше память под объекты выделять в куче, а под стандартные типы данных — на стеке.
Хорошо, спасибо. Насчёт const тоже читал что его лучше вставлять везде, где только можно
первым числом вводится строка (y), вторым — столбец (x).
Можно прибавить единичку после ввода с клавиатуры, а вообще позже хочу прикрутить GUI.
Здесь недоглядел, спасибо.
Можно пример кода? Изначально у меня было несколько однотипных функций для определения победителя, затем их объединил в одну и вызывал её с параметром-указателем на функцию.
т.е. char заменить на int, я правильно понял?
Тогда где Controller?
Ну вот чертить диаграммы здесь точно неудобно.
Можете сделать, например, классы Board, Game, Player.
Board — знает только о себе,
т.е. что, где у нее находится.
По сути — матрица.
User — знает как сделать ход (например, подождать ввода пользователя, ну или посчитать ходи, если это игрок с ИИ). Скорее всего класс абстрактный.
Game знает правила игры (или нет?),
знает как опрашивать доску,
и знает какой юзвер сейчас должен ходить.
Game не настолько большой объект, чтобы переживать за него.
Зато если вылетит исключение из game->Start(), то,
Вам сейчас нужно позаботиться о обработке исключения.
Там где это разумно и не мешает — да.
А если пихать его куда попало,
можно даже компилятору оптимизации поломать,
не говоря уже о Вашем случае,
оно там ну вот совсем не к месту.
Правило такое — если функция-член не изменяет объект,
то делаем её const, если она не выбрасывает исключений, то noexcept.
Накатал: https://github.com/croessmah/console_xo
Сделано под 4 игрока (символов больше не придумал). :D
Если надо под два, то из вектора users удаляем двоих.
Всё, больше ничего менять не надо.
Морочить голову выносом игрового цикла за main даже не стал.
Обязательно попробую переработать архитектуру, спасибо.
Хорошо, теперь более-менее разобрался с const.
Я рад, что смог заинтересовать Вас, и Вы нашли время 'накатать' по-своему.
Будет чем заняться ближайшее время, спасибо еще раз.
С наступающим :)
Ну, не совсем по-моему.
Просто я лентяй, а задумка не под консоль изначальна.
Посему, всё что связано с консолью наляпано как спагетти. )))
И Вас! Всего наилучшего. :)
Мне писать — не имеет смысла. Когда приведёшь в порядок классы, оно само у тебя сложится.
Это всё понятно. Просто неудобно.
Надо либо клавишами управления курсором выбирать позицию, либо мышью.
Кстати, первым вводится таки x (номер столбца). По крайней мере, в первой версии.
Тут есть ещё один момент. Думаем в сторону «переходим от консоли к GUI». Что нужно изменить в программе, что бы это сделать? И как нужно изменить программу, что бы такой переход потребовал замены (переписывания) только одного модуля?
Такая мысль и была изначально.
Поэтому, чтобы при переходе было минимум модификаций кода логика сейчас такая, какая она есть.
Класс Board(Model) не изменяется, только Game (View). Итак, план действий:
0) Задание настроек игры
1) Очищение консоли cls||clear, и вывод игрового поля
2) Осуществление хода
3) Определение состояния игры: ничья/победа/следующий ход
Доска — это не вся модель игры.
У игры же есть состояние, игроки и т.д.
loktionov129, понятия «модель», «представление» не надо понимать так буквально. Croessmah дал совершенно правильные рекомендации по структуре. Я просто добавил к его схеме ещё один элемент.
Ввод-вывод, UI — обычно системозависимы. Любой шаг в сторону от стандартных
<iostream>
и<cstdio>
тянет за собой системозависимые вызовы. Не говоря уж о переходе от консольного приложения к приложению с GUI. Поэтому лучше написать отдельный абстрактный класс, который будет удовлетворять все нужды игры по вводу-выводу. И от него наследовать класс, реализующий методы для работы с консолью.Когда понадобится GUI, придётся написать только класс-потомок для работы с GUI, не затрагивая другие части программы. Да, конечно изменится и функция
main
. Поэтому она не должна содержать функциональности самой игры, а служить только для её запуска.Исправил.
Сделал на Qt.
https://github.com/loktionov129/CPP_Tic-Tac-Toe
Тоже исправил.
А вот здесь не понял. Как это сделать проще ?
Если больше нет возможности составить линию указанной длины.
Например, при поле 5x5 и необходимости собрать линию из 4:
как видите, больше ни у кого нет возможности построить линию длиной в 4, т.е. дальнейшая игра — пустая трата времени.
Опять же, зависит от того, как это у Вас всё устроено.
В принципе, можно из каждой свободной клетки «пускать луч» в каждом направлении и смотреть, можно ли там составить линию заданной длины.
Если нельзя, тогда ничья. Но с самого начала выполнять эту операцию бессмысленно.
Окей, спасибо. Работаем дальше.. :)
Пока что на этот случай есть
void RestartGame(char *message = 0) override;
Как жаль что нету исходников, может еще есть где-то ? Спасибо за ответ.