{}const=>[]async()letfn</>var
PythonПрактикаАлгоритмы

Крестики-нолики на Python: поле, проверка победы и игра вдвоём

Пошагово собираем консольные крестики-нолики: список из девяти клеток, безопасный ввод, восемь линий победы, ничья и полный код.

К

Кодик

Автор

7 мин чтения

Крестики-нолики на Python удобно собирать не с интерфейса, а с модели: список из девяти клеток, восемь победных линий и цикл, который передаёт ход между X и O. В итоге получится консольная игра вдвоём с защитой от неверного номера, занятых клеток, победой и ничьей.

Проект тренирует списки, функции, циклы и проверку условий в одной понятной задаче. Сначала научимся показывать поле, затем проверим один ход, вынесем победу в отдельную функцию и только после этого соберём полный игровой цикл.

1Поле

Девять элементов списка хранят X, O или пробел.

2Правила

Восемь троек индексов описывают все линии победы.

3Цикл

После корректного хода проверяет победу, ничью и меняет игрока.

Создаём поле из списка и нумеруем клетки

Список индексируется от 0 до 8, но человеку привычнее выбирать клетки от 1 до 9. Поэтому при чтении хода вычтем единицу. Пустую клетку храним как пробел, а при выводе временно показываем её номер. Так игрок всегда видит доступные варианты.

Номер на экранеИндекс спискаПозиция
1, 2, 30, 1, 2Верхний ряд
4, 5, 63, 4, 5Средний ряд
7, 8, 96, 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. В остальном алгоритм работает с индексами списка.

🔥 100 000+ учеников уже с нами

Устал читать теорию?
Пора кодить!

Кодик — приложение, где ты учишься программировать через практику. AI-наставник, интерактивные уроки, реальные проекты.

🤖 AI 24/7
🎓 Сертификаты
💰 Бесплатно
🚀 Начать учиться
Присоединились сегодня

Принимаем ход и защищаемся от неверного ввода

У корректного хода сразу три свойства: введено целое число, оно находится от 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
Четыре проверки функции ввода
  1. Введите букву или пустую строку: программа не должна завершиться с ValueError.
  2. Введите 0 и 10: оба числа должны быть отклонены.
  3. Сделайте ход в клетку 5, затем попробуйте выбрать её снова.
  4. Введите доступный номер: функция должна вернуть индекс и закончить цикл.
Основной цикл получит безопасное значение

После 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 и имя текущего игрока. Функции получают поле явно через параметры, поэтому их можно вызвать с подготовленным примером и проверить независимо. Если позже появится графический интерфейс, этот слой правил не придётся переписывать: кнопка просто передаст выбранный индекс, а экран заново отобразит тот же список.

IndexError на числе 9

Номер пользователя записан в список без вычитания единицы. Экранные клетки 1-9 соответствуют индексам 0-8.

Можно перезаписать чужой символ

Перед присваиванием нет проверки board[position] != EMPTY. Оставьте её внутри ask_move.

Последний ход объявлен ничьёй

Заполнение поля проверяется раньше победы. Поменяйте эти две проверки местами.

Побеждают три пустые клетки

Сравниваются только значения клеток без проверки символа player. Используйте цепочку равенств с текущим игроком.

Как проверить игру и добавить компьютерного соперника

СценарийПоследовательность ходов X/OОжидание
Горизонталь X1, 4, 2, 5, 3X побеждает сверху
Диагональ X1, 2, 5, 3, 9X побеждает по диагонали
Вертикаль O1, 2, 3, 5, 4, 8O побеждает в центре
Занятая клетка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: правила похожи, но браузерный интерфейс устроен иначе.

🎯Хватит откладывать

Понравилась статья?
Пора применять на практике!

В Кодик ты не просто читаешь — ты сразу пишешь код. Теория + практика = реальный скилл.

Мгновенная практика
🧠AI объяснит код
🏆Сертификат

Без регистрации • Без карты