Эксперимент: Крестики-нолики
Пока правила в тонкостях не придумал - буду придумывать по ходу. Если других участников не будет, то буду играть сам несколько ролей насколько хватит терпения.
Ход первый: ТЗ
Техническое задание на игру крестики-нолики.
1 этап.
Процесс игры ведется в консоли. Игрок задает положение игры координатой: столбец, строка. Если координата находится вне доски или указывает на занятую ячейку, то выдается сигнал об ошибке и координата запрашивается заново.
После хода игрока должен сходить компьютер. При установке трёх одинаковых символов в линию, компьютер должен выдать сообщение о выигрыше игрока либо компьютера. Если будут заполнены все ячейки, но одинаковые символов не будут стоять на одной линии, то должно выдаться сообщение о ничейном результате.
Этап считается завершенным, когда компьютер в любом случае не проигрывает, а созданная структура кода станет красивой и читаемой.
2 этап.
Переделать игру в измененный вариант игры рендзю: игра ведется на бесконечном поле. Выигрывает тот игрок, который поставил 5 одинаковых символов в ряд.
---
Вторым ходом по науке дожен быть анализ (возможно, ООА), разбор прецедентов.
Программу планируется сделать в консоли на Питоне (для начала). Таким образом, заодно решается задача изучения Питона на практике.
Прецеденты для первой итерации программы
1. Основной (удачный) сценарий.
1.1. Игрок запускает игру.
1.2. На экран выводится поле с пустыми клетками.
1.3. Программа предлагает ввести игроку координаты символа (крестика) в поле ("столбец, строка").
1.4. Игрок вводит координату.
1.5. Программа выводит поле с символами: крестиками и ноликами.
1.6. Программа оценивает расположение символов на выигрыш, проигрыш, ничью. Если произошло одно из этих событий, то выдается соответствующее сообщение.
1.6.1. Игроку предлагается начать игру заново.
1.6.2. Если игрок соглашается, то переходим к пункту 1.2.
1.6.3. Иначе программа завершается.
1.7. Программа делает свой ход. На начальной итерации просто ставит свой символ (нолик) в свободную клетку.
1.8. То же что и 1.6 с подпунктами.
1.9. Переходим к пункту 1.3.
2. Ветки неудачных сценариев:
2.1. (вариант 1.4) Игрок вводит координату клетки, которая уже занята.
2.2. Программа выдает сообщение, что клетка уже занята.
2.3. Переходим к пункту 1.3 основного сценария.
3.1. (вариант 1.4) Игрок вводит координату клетки, которой нет на игровом поле.
3.2. Программа выдает сообщение, что такой клетки нет.
3.3. Переходим к пункту 1.3 основного сценария.
Анализ (предварительный) программы по существительным.
Есть следующие объекты: игрок, программа (искусственный противник), анализатор выигрыша, поле (доска), клетка и 2 символа – крестик и нолик.
Игрока может представлять контроллер, получающий от игрока событие ввода координаты крестика и передающий его доске. Этот же контроллер может и выдавать информацию игроку. Хотя, можно назначить и другой объект.
Виртуальный противник может непосредственно передавать координату нолика доске. Он также должен получать текущую матрицу координат от доски, чтобы не генерировать координату занятой ячейки.
Доска включает в себя 9 объектов-клеток. Основные функции доски: получать координаты новых символов и выводиться на экран. Доска может просто использовать функции вывода клеток.
Каждая клетка может быть в трёх состояниях: пустая, с крестиком, с ноликом. Так как клетка меняет свое состояние, то нет смысла делать иерархию клеток. Состояние клетки можно выделить в отдельный объект или просто использовать целочисленную переменную.
Анализатор выигрыша должен получать текущую матрицу от доски и выдавать результаты: нет события, выигрыш, проигрыш, ничья.
Очевидно, что также должен быть предусмотрен объект-диспетчер, который будет управлять другими объектами.
------
В области такого анализа я не спец. Хотелось бы услышать адекватную критику.
По науке дальше надо рисовать UML-диаграммы. Но я не нашел подходящего инструмента. Возможно, лучше всего рисовать от руки и фотографировать. Или использовать растровый графический редактор вместо школьной доски. Или можно использовать псевдо UML-графику типа:
[Board]<>------->9[Cell]
Пока не решил, что удобнее.
В принципе, в данном простом случае можно обойтись и без диаграмм. Но это не есть правильно для учебного задания.
Диаграмма UML
Автор учебника по UML Крег Ларман пишет, что специализированными редакторами надо пользоваться при составлении документации. Но перед разработкой и во время ее лучше рисовать на доске или на крупных листах от руки. Ибо диаграммы вторичны, должны помогать писать код, а не добовлять мучения в процесс. Потом можно делать качественные фотографии, если получится интересный чертеж.
У меня нет доски. На листе бумаги я пытался рисовать, но неудобно стирать резинкой - бумага портится и т.д. Пока нашел следующее компромиссное решение: рисовать черновики в растровом графичкском редакторе Paint. Выходит быстро, почти как вручную и почти так же криво...
Пока нарисовал черновик одного хода в крестиках-ноликах.
Если тема не загнется до начала июля, я бы поучоствовал тоже)
Ход 5
Перехожу к программированию. Сделаю компонент Board, который будет выводить поле примерно так:
---|---|---
| |
---|---|---
| | [/FONT]
Заготовка с классом, которую можно испытать:
matrix = [[' ' for i in xrange(3)] for i in xrange(3)]
def Draw(self):
pass
def main():
board = Board()
board.Draw()
if __name__ == "__main__":
main()
Ну и сама функция рисования. Можно сделать конкретную, типа:
line = "---|---|---"
for i in xrange(3):
t = self.matrix
print " %c | %c | %c " % (t[0], t[1], t[2])
if i != 2 :
print line
или универсальную, на произвольное количество клеток:
drawnMatrix = []
for y in self.matrix:
s = ""
for x in y:
s = s + ' ' + str(x) + " |"
s = s[:-1]
drawnMatrix.append(s)
s = ""
for x in y:
s = s + "---|"
s = s[:-1]
drawnMatrix.append(s)
drawnMatrix = drawnMatrix[:-1]
for s in drawnMatrix:
print s
но вторая трудно читаемая, так что ее можно отложить на определенное время.
Итак, основная работа с GUI сделана, дальше перейду к вводу координат от игрока и логике оппонента.
Как и планировалось приступлю к диалогу с игроком и к оппоненту. Вместо диспетчера пока буду использовать main. Должно получится что-то типа:
board = Board()
board.Draw()
user = UserContr(board)
oppnt = Opponent(board)
while(board.Analize()):
user.SetCross()
oppnt.SetO()
board.Draw()
Вроде бы, все понятно без комментариев.
Отдельного анализатора делать не буду, так как пока не планировал. Поэтому функцию анализа засуну в "доску".
Слишком маленькие шаги делать скучно, потому создам сразу сравнительно большой код:
matrix = [[' ' for i in xrange(3)] for i in xrange(3)]
def Draw(self):
line = "---|---|---"
for i in xrange(3):
t = self.matrix
print " %c | %c | %c " % (t[0], t[1], t[2])
if i != 2 :
print line
def SetXY(self, x, y, char):
self.matrix[y][x] = char
def TestXY(self, x, y):
if (y < len(self.matrix)) and (x < len(self.matrix[0]))\
and self.matrix[y][x] == ' ':
return True
else:
return False
def GetSizes(self):
return (len(self.matrix[0]), len(self.matrix))
def Analize(self):
for i in self.matrix:
for j in i:
if j == ' ':
return True
return False
class Opponent:
def __init__(self, ptrBoard):
self.board = ptrBoard
def SetO(self):
sz = self.board.GetSizes()
for i in xrange(sz[0]):
for j in xrange(sz[1]):
if self.board.TestXY(i, j):
print i, j
self.board.SetXY(i, j, 'O')
return
class UserContr:
def __init__(self, ptrBoard):
self.board = ptrBoard
def SetCross(self):
x = int(raw_input("Input X: "))
y = int(raw_input("Input Y: "))
if board.TestXY(x, y):
board.SetXY(x, y, 'X')
Теперь, если собрать все части, то уже можно "играть". Только выигрыш или проигрыш еще не рассчитывается... Надо этим заняться на следующем ходу.
Уточнение. Используется Python 2.6.2. Работа программы проверяется в IDLE.
Шаг 6.5.
Сегодня передышка в коде. Надо определиться с двумя задачами:
1. Алгоритм определения выигрыша.
2. Разделение объектов.
Подробнее.
1. Так как выигрышных комбинаций всего восемь для 9-клеточных крестиков-ноликов (3 по горизонтали, 3 по вертикали, 2 по диагонали), то можно тупо перебирать эти комбинации. Но это не очень эффективный алгоритм и его трудно перенести на пятизначные (в безграничном поле) крестики-нолики. Однако, если учитывать координату последнего хода, то можно перебирать только 2-4 комбинации. Но алгоритм немного усложнится. А может есть алгоритм еще лучше.
2. Так выходит, что матрица с крестиками-ноликами должна быть доступна и объекту доски-вида, и объекту виртуального оппонента (чтобы он выбрал лучшую стратегию), и объекту анализатора. Причем, пока я не нашел, как сделать так, чтобы матрицу мог менять только один объект, а остальные могли только просматривать. Как-то не ООПешно...
Возможно, еще надо подумать над тем, чтобы разделить код на модули.
2. базовый тип участника. абстрактный класс.
3. Ему наследуют Игрок, Компьютер(отличаются источником ввода). Реализуют ввод. Конечный автомат - "в игре","выиграш", "проиграш", "ошибочный ход", "ожидание хода противника". т.д. Взаимодействуют с диспечером.
4. Объект матрица - содержит состояние игровой доски. Управляется анализатором ходов, флаги и содержание читаются диспечером игроков.
5. Анализатор ходов - устанавливат флаги состояния матрицы.
6. Диспечер вывода взаимодействует с диспечером игроков.
7. АИ (черный ящик) - взаимодействует с объектами Компьютер и Диспечер участников.
Я в свое время реализовал примерно такое - хотя могу чего и запямятовать.
2. базовый тип участника. абстрактный класс.
3. Ему наследуют Игрок, Компьютер(отличаются источником ввода). Реализуют ввод. Конечный автомат - "в игре","выиграш", "проиграш", "ошибочный ход", "ожидание хода противника". т.д. Взаимодействуют с диспечером.
4. Объект матрица - содержит состояние игровой доски. Управляется анализатором ходов, флаги и содержание читаются диспечером игроков.
5. Анализатор ходов - устанавливат флаги состояния матрицы.
6. Диспечер вывода взаимодействует с диспечером игроков.
7. АИ (черный ящик) - взаимодействует с объектами Компьютер и Диспечер участников.
(Процитировал все, чтобы видеть на этой странице)
В общем, какая-то основа. Можно даже на нее какие-то куски моего творчества переложить. Однако, для лучшего понимания попытаюсь нарисовать диаграмму классов (по описанию), и упрощенную диаграмму последовательности для одного хода (без ошибок игрока и результата).
Из диаграммы последовательности пока не совсем ясно, зачем нужен объект "Компьютер", но, возможно, я не все правильно изобразил.
В общем, какая-то основа. Можно даже на нее какие-то куски моего творчества переложить. Однако, для лучшего понимания попытаюсь нарисовать диаграмму классов (по описанию), и упрощенную диаграмму последовательности для одного хода (без ошибок игрока и результата).
Из диаграммы последовательности пока не совсем ясно, зачем нужен объект "Компьютер", но, возможно, я не все правильно изобразил.
И еще:
Диспечер игроков (ДИ) и матрица (Диспечер игровой доски(ДИД)) наследуют одному базовому классу. ДИ управляет объектами игроков, ДИД - объектами игрового поля (в частном случае - ячейки для записи).
Диспечер вывода (ДВ) взаимодействует с ДИ и ДИД - с одного получает информацию о состоянии игроков, со второго - информацию о состоянии игровой доски. Вроде так. :)
Объект "компьютер" и "участник" по сути отличаются:
1. как перегружают функцию ввода,
2. и наличием функции-члена опроса ДВ у объекта компьютер. Объект игрок в этом не нуждался. Кстати потом в связи с введением режима сетовой игры эти объекты были переработаны - и все классы игроков были редуцированы до базового, а их функционал был передан в Диспечер ввода (ДВв)
Классы IA, кстати говоря не входят в пространство имен программы - его взаимодействие с программой происходит через объект "компьютер" (в последствии через ДВв)
Заготовку сделаю не совсем по советам kot_, а некоторый гибрид того, что я делал и его схемы. Пока главное, чтобы можно было играть.
matrix = [[' ' for i in xrange(3)] for i in xrange(3)]
steps = 0
flags = None
def TestXY(self, x, y):
if (y < len(self.matrix)) and (x < len(self.matrix[0]))\
and self.matrix[y][x] == ' ':
return True
else:
return False
def SetXY(self, x, y, char):
if self.TestXY(x, y):
self.matrix[y][x] = char
self.SetFlags(x, y, char)
return True
return False
def SetFlags(self, x, y, char):
if self.FindWin(x, y, char):
if char == 'O':
self.flags = "Opponent wins"
if char == 'X':
self.flags = "You win"
def FindWin(self, x, y, char):
# test vertical
t = self.matrix
if (t[0][x] == t[1][x] == t[2][x] == char): return char
# test horizontal
if (t[y][0] == t[y][1] == t[y][2] == char): return char
# test diagonal 1
if (t[0][0] == t[1][1] == t[2][2] == char): return char
# test diagonal 2
if (t[0][2] == t[1][1] == t[2][0] == char): return char
return None
def StepEnable(self):
if self.steps < 9:
return True
self.flags = "Nobody wins"
return False
class Board:
def Draw(self, matrix):
line = "---|---|---"
for i in xrange(3):
t = matrix
print " %c | %c | %c " % (t[0], t[1], t[2])
if i != 2 :
print line
def PrintResult(self, result):
print result
class Opponent:
def __init__(self, ptrMatrix):
self.matrix = ptrMatrix
def GetO(self):
for i in xrange(3):
for j in xrange(3):
if self.matrix[j] == ' ':
return (i, j)
class UserContr:
def GetCross(self):
x = int(raw_input("Input X: "))
y = int(raw_input("Input Y: "))
return (x, y)
class Dispatcher:
def __init__(self):
pass
def Run(self):
analizer = Analyzer()
board = Board()
board.Draw(analizer.matrix)
user = UserContr()
oppnt = Opponent(analizer.matrix)
while(analizer.StepEnable()):
coord = user.GetCross()
while(not analizer.SetXY(coord[0], coord[1], 'X')):
coord = user.GetCross()
if analizer.flags:
board.Draw(analizer.matrix)
board.PrintResult(analizer.flags)
return
analizer.steps = analizer.steps + 1
if not analizer.StepEnable(): break
coord = oppnt.GetO()
analizer.SetXY(coord[1], coord[0], 'O')
analizer.steps = analizer.steps + 1
if analizer.flags:
board.Draw(analizer.matrix)
board.PrintResult(analizer.flags)
return
board.Draw(analizer.matrix)
print analizer.flags
if __name__ == "__main__":
dispatcher = Dispatcher()
dispatcher.Run()
Играть уже можно (в IDLE). Легко выиграть, легко проиграть, труднее добиться ничьей.
Недостатки первой версии (в произвольном порядке)
1. Функция Run класса Dispatcher слишком длинная. Фактически этот класс является оберткой для одной функции.
2. В функции Run класса Dispatcher есть ненужное дублирование.
3. Класс Analyzer содержит слишком много функций по сравнению с другими классами. Это маленький недостаток.
4. Путаница с координатами в матрице: установка символа ведется в порядке y, x, а опрос игроков ведется в порядке x, y. В результате, повышается ошибкоемкость кода.
5. Функции некоторых классов не нуждаются в этих классах. Можно убрать классы, либо сделать эти функции статическими.
6. Код программы разросся. Возможно, следует поделить его на модули.
7. Много ненужной конкретики. Например, используется "магическое" число 3, "магические" символы 'X' и 'O'. Кроме того, функция вывода матрицы и функция определения выиграша слишком сильно привязаны к конкретной матрице.
8. Оппонент предсказуем. Он не пытается победить, а просто ставит нолики по порядку.
9. Игрок всегда ходит первым.
10. Нет защиты от дурака: ложный ввод может привести к краху программы.
11. Нет запроса на запуск новой игры. То есть, чтобы сыграть еще раз надо запустить программу заново.
Достоинства:
1. Задачи разделены по классам в соответствии с их смысловым назначением. Почти.
2. Один класс отвечает за ввод, один на вывод. То есть, проще будет перенести в вариант с GUI.
В общем, исправлять надо многое.
Совместной публичной разработки так не получится - надо было заранее продумать и договориться как она будет проходить. Если бы я лучше знал Питон, то можно было бы привратить это в мастер-класс. Но пока я его знаю плохо.
В другое время энтузиастов наверняка было бы побольше.
PS: судя по первой странице - человека этак три еще
Тогда подожду до августа. Хотя до того времени я могу увлечься каким-нибудь Boo )))