Крестики-нолики на Python удобно собирать не с интерфейса, а с модели: список из девяти клеток, восемь победных линий и цикл, который передаёт ход между X и O. В итоге получится консольная игра вдвоём с защитой от неверного номера, занятых клеток, победой и ничьей.
Проект тренирует списки, функции, циклы и проверку условий в одной понятной задаче. Сначала научимся показывать поле, затем проверим один ход, вынесем победу в отдельную функцию и только после этого соберём полный игровой цикл.
Девять элементов списка хранят X, O или пробел.
Восемь троек индексов описывают все линии победы.
После корректного хода проверяет победу, ничью и меняет игрока.
Создаём поле из списка и нумеруем клетки
Список индексируется от 0 до 8, но человеку привычнее выбирать клетки от 1 до 9. Поэтому при чтении хода вычтем единицу. Пустую клетку храним как пробел, а при выводе временно показываем её номер. Так игрок всегда видит доступные варианты.
| Номер на экране | Индекс списка | Позиция |
|---|---|---|
| 1, 2, 3 | 0, 1, 2 | Верхний ряд |
| 4, 5, 6 | 3, 4, 5 | Средний ряд |
| 7, 8, 9 | 6, 7, 8 | Нижний ряд |
EMPTY = ' '
board = [EMPTY] * 9
def show_board(board):
cells = [
value if value != EMPTY else str(index + 1)
for index, value in enumerate(board)
]
print(f' {cells[0]} | {cells[1]} | {cells[2]} ')
print('---+---+---')
print(f' {cells[3]} | {cells[4]} | {cells[5]} ')
print('---+---+---')
print(f' {cells[6]} | {cells[7]} | {cells[8]} ')
show_board(board)Функция вывода создаёт отдельный список cells и не меняет настоящее поле. В нём пустые пробелы временно заменяются номерами, а X и O остаются как есть. Это важная граница: представление можно менять сколько угодно, но состояние партии должно обновляться только в момент принятого хода. Иначе простой вызов show_board() неожиданно изменит правила.

Преобразование выполняется один раз: position = number - 1. В остальном алгоритм работает с индексами списка.
Принимаем ход и защищаемся от неверного ввода
У корректного хода сразу три свойства: введено целое число, оно находится от 1 до 9, выбранная клетка пуста. Проверяем их по очереди и повторяем вопрос в цикле. Функция возвращает только готовый индекс, поэтому основной игровой цикл не знает, сколько ошибок сделал игрок.
def ask_move(board, player):
while True:
raw = input(f'Ход {player}. Выберите клетку 1-9: ')
if not raw.isdigit():
print('Введите целое число.')
continue
number = int(raw)
if number < 1 or number > 9:
print('Номер должен быть от 1 до 9.')
continue
position = number - 1
if board[position] != EMPTY:
print('Эта клетка уже занята.')
continue
return position- Введите букву или пустую строку: программа не должна завершиться с ValueError.
- Введите 0 и 10: оба числа должны быть отклонены.
- Сделайте ход в клетку 5, затем попробуйте выбрать её снова.
- Введите доступный номер: функция должна вернуть индекс и закончить цикл.
После position = ask_move(board, player) можно без дополнительных условий записать символ в board[position]. Все проверки уже выполнены внутри функции.
Проверка isdigit() намеренно выполняется до int(). Без неё буква вызвала бы ValueError и завершила игру. Отрицательное число тоже не проходит isdigit() из-за знака минус, что для диапазона 1-9 нам подходит. Для более общего числового ввода применяют try/except ValueError, но здесь короткая проверка выражает правило проще.
Проверяем восемь линий победы
Победа состоит из трёх горизонталей, трёх вертикалей и двух диагоналей. Запишем каждую линию кортежем индексов. Функция перебирает эти кортежи и возвращает True, если во всех трёх клетках стоит символ текущего игрока.
WINNING_LINES = [
(0, 1, 2), (3, 4, 5), (6, 7, 8),
(0, 3, 6), (1, 4, 7), (2, 5, 8),
(0, 4, 8), (2, 4, 6),
]
def has_won(board, player):
return any(
board[a] == board[b] == board[c] == player
for a, b, c in WINNING_LINES
)Выражение с any() отвечает: существует ли хотя бы одна подходящая линия. Его можно переписать обычным циклом с ранним return True, результат будет тем же. Важно не проверять лишь равенство трёх клеток без символа игрока, иначе три пустых пробела тоже будут выглядеть как линия.

Правила хранятся в данных. Функция проверки остаётся одинаковой для горизонтали, вертикали и диагонали.
['X', 'X', 'X', ' ', 'O', ' ', 'O', ' ', ' '] и вызовите has_won(board, 'X'). Так каждая линия проверяется за секунду.Собираем полную игру вдвоём
После каждого корректного хода порядок всегда одинаков: показать поле, проверить победу, проверить заполнение, сменить игрока. Проверка ничьей идёт после победы, потому что последний свободный ход способен одновременно заполнить поле и создать победную линию.
EMPTY = ' '
WINNING_LINES = [
(0, 1, 2), (3, 4, 5), (6, 7, 8),
(0, 3, 6), (1, 4, 7), (2, 5, 8),
(0, 4, 8), (2, 4, 6),
]
def show_board(board):
cells = [value if value != EMPTY else str(i + 1)
for i, value in enumerate(board)]
print(f' {cells[0]} | {cells[1]} | {cells[2]} ')
print('---+---+---')
print(f' {cells[3]} | {cells[4]} | {cells[5]} ')
print('---+---+---')
print(f' {cells[6]} | {cells[7]} | {cells[8]} ')
def ask_move(board, player):
while True:
raw = input(f'Ход {player}. Клетка 1-9: ')
if not raw.isdigit():
print('Нужно целое число.')
continue
position = int(raw) - 1
if position not in range(9):
print('Выберите число от 1 до 9.')
continue
if board[position] != EMPTY:
print('Клетка занята.')
continue
return position
def has_won(board, player):
return any(board[a] == board[b] == board[c] == player
for a, b, c in WINNING_LINES)
board = [EMPTY] * 9
player = 'X'
show_board(board)
while True:
position = ask_move(board, player)
board[position] = player
show_board(board)
if has_won(board, player):
print(f'Игрок {player} победил!')
break
if EMPTY not in board:
print('Ничья!')
break
player = 'O' if player == 'X' else 'X'
Смена игрока выполняется только тогда, когда партия не закончилась победой или ничьей.
В основном цикле есть одно изменяемое состояние: список board и имя текущего игрока. Функции получают поле явно через параметры, поэтому их можно вызвать с подготовленным примером и проверить независимо. Если позже появится графический интерфейс, этот слой правил не придётся переписывать: кнопка просто передаст выбранный индекс, а экран заново отобразит тот же список.
Номер пользователя записан в список без вычитания единицы. Экранные клетки 1-9 соответствуют индексам 0-8.
Перед присваиванием нет проверки board[position] != EMPTY. Оставьте её внутри ask_move.
Заполнение поля проверяется раньше победы. Поменяйте эти две проверки местами.
Сравниваются только значения клеток без проверки символа player. Используйте цепочку равенств с текущим игроком.
Как проверить игру и добавить компьютерного соперника
| Сценарий | Последовательность ходов X/O | Ожидание |
|---|---|---|
| Горизонталь X | 1, 4, 2, 5, 3 | X побеждает сверху |
| Диагональ X | 1, 2, 5, 3, 9 | X побеждает по диагонали |
| Вертикаль O | 1, 2, 3, 5, 4, 8 | O побеждает в центре |
| Занятая клетка | 1, затем снова 1 | Ход повторно запрашивается |
Для простого компьютера соберите список индексов пустых клеток и выберите один через random.choice(). Не смешивайте этот выбор с проверкой победы. Сначала замените только способ получения позиции для O, а правила и основной цикл оставьте прежними.
from random import choice
def computer_move(board):
available = [
index for index, value in enumerate(board)
if value == EMPTY
]
return choice(available)Почему поле хранится в одном списке, а не в трёх?
Один список проще передавать в функции, перебирать и проверять на заполнение. Ряды восстанавливаются по индексам.
Зачем показывать номера вместо пустых клеток?
Игрок сразу видит допустимый ввод и не держит раскладку 1-9 в памяти.
Почему сначала проверяется победа, затем ничья?
Последний ход может заполнить поле и одновременно завершить победную линию. Победа должна иметь приоритет.
Можно ли сделать поле двумерным?
Да, но для размера 3 на 3 плоский список уменьшает код. Двумерная модель полезнее при переменном размере поля.
Как добавить графический интерфейс?
Сохраните board и функции правил, а show_board и ask_move замените кнопками и обновлением подписей.
Закрепите функции, циклы и списки в курсе Python. Затем добавьте случайного соперника или сохранение счёта серии. Другие идеи есть в подборке 10 мини-проектов на Python.
Для сравнения архитектуры можно посмотреть версию крестиков-ноликов на JavaScript: правила похожи, но браузерный интерфейс устроен иначе.
