Last active
November 24, 2018 09:00
-
-
Save tirinox/4efdc240902d88bfc4261d4c0af8aa33 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
from flask import Flask, request | |
class Game: | |
X = 'X' | |
O = 'O' | |
N = ' ' | |
def __init__(self, size=3, to_win=3): | |
""" | |
:param size: Размер стороны квадрата поля | |
:param to_win: Скольно нужно поставить в одну линию, чтобы победить | |
""" | |
assert 3 <= size <= 30 | |
assert 2 <= to_win <= size | |
self.size = size | |
self._to_win = to_win | |
# текущий ход - Х | |
self._turn = self.X | |
# поле будет линейный массив сначала первая строка, потом вторая и т.д. | |
self._field = [self.N for _ in range(0, self.size * self.size)] | |
# тут будем хранить координаты победной линии (чтобы выделить ее в конце игры) | |
self.win_line = [] | |
def _get_field(self, x, y): | |
""" | |
Получить состояние клетки по координатам (x, y) | |
При этом 0, 0 - верхний левый угол | |
:param x: колонка | |
:param y: ряд | |
:return: состояние клетки (X, O, N) или None (координаты за пределами) | |
""" | |
if 0 <= x < self.size and 0 <= y < self.size: | |
return self._field[x + y * self.size] | |
else: | |
return None | |
def _set_field(self, x, y, symbol): | |
""" | |
Записать в пустую ячейку крестик или нолик | |
:param x: колонка | |
:param y: ряд | |
:param symbol: символ | |
:return: Bool удалось ли записать | |
""" | |
cur_symbol = self._get_field(x, y) | |
if cur_symbol == self.N and symbol in [self.X, self.O]: | |
self._field[x + y * self.size] = symbol | |
return True | |
else: | |
return False | |
def _change_turn(self): | |
""" | |
Просто передает ход другому игроку | |
:return: | |
""" | |
if self._turn == self.X: | |
self._turn = self.O | |
else: | |
self._turn = self.X | |
def is_field_full(self): | |
""" | |
Проверяет, заполнилось ли поле целиком | |
:return: bool | |
""" | |
for i in range(0, self.size * self.size): | |
if self._field[i] == self.N: | |
return False | |
return True | |
def _scan_line(self, x0, y0, dx, dy): | |
""" | |
Сканируем поле с точки x0, y0 в напралвении dx, dy | |
При этом заполняет win_line (выигрышная линия) парами (x, y) | |
:param x0: начало х | |
:param y0: начало у | |
:param dx: сдвиг по х | |
:param dy: сдвиг по у | |
:return: возвращает None, если по этой линии нет выигрыша, | |
или возвращает X или О (если линия выигрышная) | |
""" | |
assert abs(dx) + abs(dy) > 0 | |
self.win_line = [] | |
owner = None # чья линия? | |
x, y = x0, y0 | |
for step in range(self._to_win): # максимум _to_win шагов, не больше, не меньше | |
cell = self._get_field(x, y) | |
if cell is None or cell == self.N: # если мы вышли за пределы или клетка пуста | |
return None | |
if owner is None: # если клетка не пуста, то узнаем, кто будет владеть линией | |
owner = cell | |
elif owner != cell: # если владелец был определон на прошлых шагах, но этой клеткой владеет не он | |
return None | |
# пока идем хорошо, заполняем линию | |
self.win_line.append((x, y)) | |
x += dx | |
y += dy | |
return owner | |
TIED = 'tied' | |
INVALID = 'invalid' | |
PLAYING = 'playing' | |
def is_game_over(self): | |
""" | |
Проверяет, не кончилась ли игра | |
:return: X или O (победители) или TIED (ничья) или PLAYING (игра не окончена) | |
""" | |
for x in range(self.size): | |
for y in range(self.size): | |
# из каждой клетки сканируем в 4 направления | |
# впрваво, вниз, вправо-вниз, влево-вниз | |
r = self._scan_line(x, y, 1, 0) or \ | |
self._scan_line(x, y, 0, 1) or \ | |
self._scan_line(x, y, 1, 1) or \ | |
self._scan_line(x, y, -1, 1) | |
# до первого выигрышного результата | |
if r is not None: | |
return r | |
# нет победителя - нет его линии | |
self.win_line = [] | |
# если поле заполнено, но победительн не выявлен в цилке сверху, то - ничья | |
if self.is_field_full(): | |
return Game.TIED | |
else: # иначе - играем | |
return Game.PLAYING | |
def do_turn(self, x, y): | |
""" | |
Попытаемся сделать ход игроку | |
:param x: коорд. хода х | |
:param y: коорд. хода y | |
:return: результат игры (см. is_game_over) или INVALID (запрещенный ход) | |
""" | |
if self._set_field(x, y, self._turn): | |
self._change_turn() | |
return self.is_game_over() | |
else: | |
return Game.INVALID | |
def do_turn_ai(self): | |
""" | |
Сделать ход Искуственному интелленкту | |
:return: (см. do_turn) | |
""" | |
empty_cells = [i for i, cell in enumerate(self._field) if cell == self.N] | |
if len(empty_cells) > 0: | |
turn_index = random.choice(empty_cells) | |
x = int(turn_index % self.size) | |
y = int(turn_index / self.size) | |
return self.do_turn(x, y) | |
else: | |
return self.is_game_over() | |
def tag(t, c, **attributes): | |
""" | |
Оборачивает строку в HTML тэг | |
tag('a', 'Click me!', href='http://site.com') => <a href="http://site.com">Click me!</a> | |
:param t: тэг | |
:param c: содержание | |
:param attributes: именованые параметры атрибутов | |
:return: обернутая в тэг строка | |
""" | |
attrs = [f'{name}="{value}"' for name, value in attributes.items()] | |
return f'<{t} {" ".join(attrs)}>{c}</{t}>' | |
def redirect_js(to_url): | |
""" | |
Редирект через JS (будем писать его в onclick) | |
Из браузера сообщим серверу о действии игрока | |
:param to_url: куда перенаправить | |
:return: JS код | |
""" | |
return f"window.location = '{to_url}'" | |
def td(content, onclick='', bg_color='#fff'): | |
""" | |
Ячейка таблицы | |
:param content: содержимое | |
:param onclick: действиее при клике | |
:param bg_color: цвет фона | |
:return: HTML код <td>... | |
""" | |
return tag('td', content, | |
width=65, | |
height=65, | |
align='center', | |
style=f'font-size: 38pt; background: {bg_color}', | |
onclick=onclick) | |
def render_game_to_string(game: Game): | |
""" | |
Рендерим игру в HTML | |
:param game: игра | |
:return: HTML код | |
""" | |
# не кончилась ли игра? | |
state = game.is_game_over() | |
if state == game.TIED: | |
header1 = 'Ничья!' | |
elif state == game.X: | |
header1 = 'Победили крестики (вы)!' | |
elif state == game.O: | |
header1 = 'Победили нолики!' | |
else: | |
header1 = 'Ваш ход (Х)' | |
# заголовок | |
header = tag('h1', header1) + '<br>' + tag('a', 'Новая игра', href='/?action=new') + '<br>' | |
# игровое поле | |
rows = '' | |
for y in range(game.size): # во всем рядам | |
row = '' | |
for x in range(game.size): # по клетке в ряду | |
# берем клетку | |
cell_state = game._get_field(x, y) | |
# событие по клику делаем, только если клетка пустая (N) - редирект на эту же страницу с действием action=turn и координатами | |
onclick = redirect_js(f'/?action=turn&x={x}&y={y}') if cell_state == game.N else '' | |
# если координаты на выигрышной линии - закрасим их красным, иначе фон - белый | |
color = '#f99' if (x, y) in game.win_line else '#fff' | |
# добавим ячейку в ряд | |
row += td(cell_state, onclick=onclick, bg_color=color) | |
# каждый ряд обернут в тэг tr | |
rows += tag('tr', row) | |
# знаю, что border уже не используют, но нам проще всего | |
field_html = tag('table', rows, border='1') | |
# финализируем html | |
html = header + field_html | |
html = tag('body', html) | |
html = tag('html', html) | |
return html | |
def new_game(): | |
""" | |
Создает новую игру (6 клеток, 4 в линии для победы) | |
:return: Game | |
""" | |
return Game(size=6, to_win=4) | |
game = new_game() | |
app = Flask(__name__) | |
@app.route('/') | |
def index(): | |
""" | |
Обработка запроса к игре через бразуер | |
:return: | |
""" | |
# в Python есть global! WOW | |
global game | |
# действие | |
action = request.args.get('action') | |
# если требуют делать ход, и мы еще играем | |
if action == 'turn' and game.is_game_over() == game.PLAYING: | |
# координаты хода | |
x = int(request.args.get('x')) | |
y = int(request.args.get('y')) | |
# делаем ход | |
result = game.do_turn(x, y) | |
# если еще играем | |
if result == game.PLAYING: | |
# ход делает интеллект | |
game.do_turn_ai() | |
else: | |
# в любой непонятной ситуации - новая игра | |
game = new_game() | |
# а возвращем мы HTML код с видом нашей игры | |
return render_game_to_string(game) | |
# запуск веб-сервера | |
app.run(port=10345, host='localhost') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Скриншот
