Загрузка данных


# -*- coding: utf-8 -*-
"""
Короткие нарды (Backgammon) на Python + Tkinter.
Игра на одном компьютере, два игрока.
Управление: клик по своей шашке -> клик по подсвеченной точке (или по правому полю для выноса).
"""

import tkinter as tk
import random

WHITE = 1
BLACK = -1

# ---------- Геометрия ----------
CANVAS_W, CANVAS_H = 980, 680
BX0, BX1 = 60, 900            # границы доски
BY0, BY1 = 70, 630
BAR0, BAR1 = 455, 505         # "бар" посередине
PW = (BAR1 - BX0) / 6.0       # ширина одной точки
POINT_H = 240                 # высота треугольника
R = 24                        # радиус шашки

LIGHT_PT = '#f2e3c6'
DARK_PT = '#a0522d'
BOARD_BG = '#c8a165'

PIPS = {
    1: [(0, 0)],
    2: [(-1, -1), (1, 1)],
    3: [(-1, -1), (0, 0), (1, 1)],
    4: [(-1, -1), (1, -1), (-1, 1), (1, 1)],
    5: [(-1, -1), (1, -1), (0, 0), (-1, 1), (1, 1)],
    6: [(-1, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (1, 1)],
}


# ---------- Логика ----------

def point_x(i):
    """X-координата центра точки с индексом i (0..23)."""
    if 0 <= i <= 5:      # нижний правый: 0 — у правого края
        return BAR1 + (5 - i + 0.5) * PW
    if 6 <= i <= 11:     # нижний левый: 6 — у бара
        return BAR0 - (i - 6 + 0.5) * PW
    if 12 <= i <= 17:    # верхний левый: 12 — у левого края
        return BX0 + (i - 12 + 0.5) * PW
    return BAR1 + (i - 18 + 0.5) * PW   # верхний правый: 18 — у бара


def home_range(p):
    return (0, 5) if p == WHITE else (18, 23)


def direction(p):
    return -1 if p == WHITE else 1


def entry_index(p, d):
    """Куда встаёт шашка с бара при выпадении d."""
    return 24 - d if p == WHITE else d - 1


def dist(i, p):
    """Сколько очков осталось до выноса с точки i."""
    return i + 1 if p == WHITE else 24 - i


def can_land(board, i, p):
    """Можно ли встать на точку i (не более 1 чужой шашки)."""
    return board[i] * p > -2


def all_in_home(board, bar, p):
    if bar[p] > 0:
        return False
    h0, h1 = home_range(p)
    for i in range(24):
        if board[i] * p > 0 and not (h0 <= i <= h1):
            return False
    return True


def can_bear_off(board, i, d, p):
    di = dist(i, p)
    if d == di:
        return True
    if d > di:
        for j in range(24):
            if board[j] * p > 0 and dist(j, p) > di:
                return False
        return True
    return False


def generate_moves(board, bar, p, d):
    """Список возможных ходов (откуда, куда) для одной кости d."""
    res = []
    if bar[p] > 0:
        idx = entry_index(p, d)
        if can_land(board, idx, p):
            res.append(('bar', idx))
        return res

    homing = all_in_home(board, bar, p)
    dirn = direction(p)
    h0, h1 = home_range(p)

    for i in range(24):
        if board[i] * p <= 0:
            continue
        t = i + dirn * d
        if 0 <= t <= 23 and can_land(board, t, p):
            res.append((i, t))
        if homing and h0 <= i <= h1 and can_bear_off(board, i, d, p):
            res.append((i, 'off'))
    return res


def apply_move(board, bar, off, p, mv):
    frm, to = mv
    if frm == 'bar':
        bar[p] -= 1
    else:
        board[frm] -= p

    if to == 'off':
        off[p] += 1
    else:
        if board[to] * p == -1:      # выбиваем чужую шашку
            board[to] += p
            bar[-p] += 1
        board[to] += p


def gen_sequences(board, bar, player, dice):
    """Все максимально длинные последовательности ходов за этот бросок."""
    if dice[0] == dice[1]:
        remaining = [dice[0]] * 4
    else:
        remaining = list(dice)

    memo = {}

    def rec(bd, br, rem):
        key = (tuple(bd), br[WHITE], br[BLACK], tuple(sorted(rem)))
        if key in memo:
            return memo[key]

        out = []
        for d in sorted(set(rem), reverse=True):
            for mv in generate_moves(bd, br, player, d):
                nbd = bd[:]
                nbr = dict(br)
                frm, to = mv
                if frm == 'bar':
                    nbr[player] -= 1
                else:
                    nbd[frm] -= player
                if to != 'off':
                    if nbd[to] * player == -1:
                        nbd[to] += player
                        nbr[-player] += 1
                    nbd[to] += player

                nrem = list(rem)
                nrem.remove(d)
                subs = rec(nbd, nbr, nrem)
                for s in subs:
                    out.append([(mv, d)] + s)

        if not out:
            out = [[]]
        memo[key] = out
        return out

    seqs = rec(list(board), dict(bar), remaining)

    # убираем дубликаты
    uniq, seen = [], set()
    for s in seqs:
        t = tuple(s)
        if t not in seen:
            seen.add(t)
            uniq.append(s)
    seqs = uniq

    mx = max((len(s) for s in seqs), default=0)
    seqs = [s for s in seqs if len(s) == mx]

    # если играется только одна кость — должна играться старшая
    if mx == 1 and dice[0] != dice[1]:
        big = max(dice)
        seqs = [s for s in seqs if s[0][1] == big]

    return seqs


def hit_point(x, y):
    """Какая точка (или 'bar') под курсором."""
    if y < BY0 or y > BY1:
        return None
    if BAR0 <= x <= BAR1:
        return 'bar'

    mid = (BY0 + BY1) / 2.0
    top = y < mid

    if x > BAR1:
        col = int((x - BAR1) / PW)
        col = min(max(col, 0), 5)
        return 18 + col if top else 5 - col
    elif x < BAR0:
        col = int((x - BX0) / PW)
        col = min(max(col, 0), 5)
        return 12 + col if top else 6 + col
    return None


# ---------- Интерфейс ----------

class Backgammon:
    def __init__(self, root):
        self.root = root
        self.canvas = tk.Canvas(root, width=CANVAS_W, height=CANVAS_H,
                                bg='#2b1d12', highlightthickness=0)
        self.canvas.pack()
        self.canvas.bind('<Button-1>', self.on_click)
        self.gen = 0
        self.restart()

    # ----- запуск / новая игра -----
    def restart(self):
        self.gen += 1
        self.board = [0] * 24
        self.board[23] = 2      # белые: 24-й пункт
        self.board[12] = 5      # 13-й
        self.board[7] = 3       # 8-й
        self.board[5] = 5       # 6-й
        self.board[0] = -2      # чёрные: 24-й
        self.board[11] = -5     # 13-й
        self.board[16] = -3     # 8-й
        self.board[18] = -5     # 6-й

        self.bar = {WHITE: 0, BLACK: 0}
        self.off = {WHITE: 0, BLACK: 0}
        self.turn = WHITE
        self.dice = [1, 1]
        self.played = []
        self.sequences = []
        self.selected = None
        self.roll()

    # ----- ход / бросок -----
    def roll(self):
        self.selected = None
        self.played = []
        self.dice = [random.randint(1, 6), random.randint(1, 6)]
        self.sequences = gen_sequences(self.board, self.bar, self.turn, self.dice)
        self.draw()
        if not self.next_moves():
            self.draw()
            self.root.after(900, lambda g=self.gen: self.pass_turn(g))

    def pass_turn(self, gen):
        if gen != self.gen:
            return
        self.turn = -self.turn
        self.roll()

    def next_moves(self):
        n = len(self.played)
        out = []
        for seq in self.sequences:
            if len(seq) > n and seq[:n] == self.played:
                if seq[n] not in out:
                    out.append(seq[n])
        return out

    def remaining_dice(self):
        if self.dice[0] == self.dice[1]:
            rem = [self.dice[0]] * 4
        else:
            rem = list(self.dice)
        for _, d in self.played:
            if d in rem:
                rem.remove(d)
        return rem

    # ----- обработка кликов -----
    def on_click(self, event):
        if self.off[WHITE] == 15 or self.off[BLACK] == 15:
            return
        nxt = self.next_moves()
        if not nxt:
            return

        x, y = event.x, event.y

        if self.selected is not None:
            dst = self.hit_dest(x, y, nxt)
            if dst is not None:
                self.make_move(self.selected, dst)
                return

        src = self.hit_source(x, y, nxt)
        self.selected = src if src is not None else None
        self.draw()

    def hit_source(self, x, y, nxt):
        srcs = set(mv[0][0] for mv in nxt)
        p = hit_point(x, y)
        if p == 'bar':
            return 'bar' if 'bar' in srcs else None
        if p is not None and p in srcs:
            return p
        return None

    def hit_dest(self, x, y, nxt):
        dsts = set()
        for mv, _ in nxt:
            if mv[0] == self.selected:
                dsts.add(mv[1])
        if 'off' in dsts and x > BX1 + 5:
            return 'off'
        p = hit_point(x, y)
        if p is not None and p != 'bar' and p in dsts:
            return p
        return None

    def make_move(self, src, dst):
        n = len(self.played)
        for seq in self.sequences:
            if len(seq) > n and seq[:n] == self.played:
                mv, d = seq[n]
                if mv == (src, dst):
                    apply_move(self.board, self.bar, self.off, self.turn, mv)
                    self.played.append((mv, d))
                    self.selected = None
                    self.draw()

                    if self.off[self.turn] == 15:
                        return
                    if not self.next_moves():
                        self.root.after(600, lambda g=self.gen: self.pass_turn(g))
                    return

    # ----- отрисовка -----
    def stack_pos(self, i, cnt):
        x = point_x(i)
        top = i >= 12
        if cnt <= 1:
            step = 0
        else:
            step = min(50.0, (POINT_H - 2 * R - 12) / (cnt - 1))
            step = max(step, 10.0)
        out = []
        for k in range(cnt):
            y = (BY0 + R + 6 + k * step) if top else (BY1 - R - 6 - k * step)
            out.append((x, y))
        return out

    def bar_pos(self, p, cnt):
        x = (BAR0 + BAR1) / 2.0
        if cnt <= 1:
            step = 0
        else:
            half = (BY1 - BY0) / 2.0 - R - 12
            step = min(40.0, half / (cnt - 1))
            step = max(step, 8.0)
        out = []
        for k in range(cnt):
            y = (BY1 - R - 8 - k * step) if p == WHITE else (BY0 + R + 8 + k * step)
            out.append((x, y))
        return out

    def off_pos(self, p, cnt):
        x = BX1 + 42
        out = []
        for k in range(cnt):
            y = (BY1 - 14 - k * 24) if p == WHITE else (BY0 + 14 + k * 24)
            out.append((x, y))
        return out

    def draw_point(self, i):
        x = point_x(i)
        top = i >= 12
        color = LIGHT_PT if i % 2 == 0 else DARK_PT
        half = PW / 2 - 1
        if top:
            pts = [x - half, BY0, x + half, BY0, x, BY0 + POINT_H]
        else:
            pts = [x - half, BY1, x + half, BY1, x, BY1 - POINT_H]
        self.canvas.create_polygon(pts, fill=color, outline='#7a5a3a', width=1)

    def draw_checker(self, x, y, p, r, highlight=False):
        c = self.canvas
        if p == WHITE:
            fill, outline, inner = '#f7f3ea', '#3a3a3a', '#cfc7b8'
        else:
            fill, outline, inner = '#2b2b2b', '#9a9a9a', '#111111'
        c.create_oval(x - r, y - r, x + r, y + r, fill=fill, outline=outline, width=2)
        if r > 14:
            c.create_oval(x - r * 0.62, y - r * 0.62, x + r * 0.62, y + r * 0.62,
                          fill='', outline=inner, width=2)
        if highlight:
            c.create_oval(x - r - 3, y - r - 3, x + r + 3, y + r + 3,
                          outline='#ffd400', width=3)

    def draw_die(self, cx, cy, v, dim=False):
        c = self.canvas
        s = 40
        fill = '#c9c9c9' if dim else '#ffffff'
        outline = '#8a8a8a' if dim else '#333333'
        c.create_rectangle(cx - s / 2, cy - s / 2, cx + s / 2, cy + s / 2,
                           fill=fill, outline=outline, width=2)
        r = s * 0.09
        off = s * 0.27
        pc = '#9a9a9a' if dim else '#222222'
        for dx, dy in PIPS[v]:
            x, y = cx + dx * off, cy + dy * off
            c.create_oval(x - r, y - r, x + r, y + r, fill=pc, outline='')

    def draw_dice(self):
        rem = self.remaining_dice()
        if not rem:
            return
        s, gap = 40, 10
        total = len(rem) * s + (len(rem) - 1) * gap
        x0 = 480 - total / 2 + s / 2
        y = (BY0 + BY1) / 2
        for k, v in enumerate(rem):
            self.draw_die(x0 + k * (s + gap), y, v)

    def draw_dest_marker(self, dst):
        c = self.canvas
        if dst == 'off':
            x = BX1 + 42
            y = (BY1 - 120) if self.turn == WHITE else (BY0 + 120)
            c.create_oval(x - 32, y - 80, x + 32, y + 80, outline='#00ff44', width=3)
        else:
            x = point_x(dst)
            top = dst >= 12
            y = BY0 + POINT_H - 22 if top else BY1 - POINT_H + 22
            c.create_oval(x - 15, y - 15, x + 15, y + 15, outline='#00ff44', width=3)

    def status_text(self):
        if self.off[WHITE] == 15:
            return 'Победа белых!  Нажмите «Новая игра»'
        if self.off[BLACK] == 15:
            return 'Победа чёрных!  Нажмите «Новая игра»'
        who = 'Белые' if self.turn == WHITE else 'Чёрные'
        return f'Ход: {who}     Кубики: {self.dice[0]}-{self.dice[1]}'

    def draw(self):
        c = self.canvas
        c.delete('all')

        c.create_text(CANVAS_W // 2, 30, text=self.status_text(),
                      fill='#f0dcc0', font=('Arial', 16, 'bold'))

        c.create_rectangle(BX0 - 14, BY0 - 14, BX1 + 14, BY1 + 14,
                           fill='#5c3a1e', outline='#33200f', width=3)
        c.create_rectangle(BX0, BY0, BX1, BY1, fill=BOARD_BG, outline='')

        for i in range(24):
            self.draw_point(i)

        c.create_rectangle(BAR0, BY0, BAR1, BY1, fill='#6b4423',
                           outline='#3d2611', width=2)

        # шашки на точках
        for i in range(24):
            n = self.board[i]
            if n == 0:
                continue
            p = WHITE if n > 0 else BLACK
            cnt = abs(n)
            for (x, y) in self.stack_pos(i, cnt):
                self.draw_checker(x, y, p, R, highlight=(self.selected == i))

        # шашки на баре
        for p in (WHITE, BLACK):
            cnt = self.bar[p]
            if cnt == 0:
                continue
            hl = (self.selected == 'bar' and p == self.turn)
            for (x, y) in self.bar_pos(p, cnt):
                self.draw_checker(x, y, p, R, hl)

        # вынесенные шашки
        for p in (WHITE, BLACK):
            cnt = self.off[p]
            if cnt == 0:
                continue
            for (x, y) in self.off_pos(p, cnt):
                self.draw_checker(x, y, p, 11)

        # маркеры возможных ходов
        if self.selected is not None:
            dsts = set()
            for mv, _ in self.next_moves():
                if mv[0] == self.selected:
                    dsts.add(mv[1])
            for dst in dsts:
                self.draw_dest_marker(dst)

        self.draw_dice()


def main():
    root = tk.Tk()
    root.title('Нарды')
    root.configure(bg='#2b1d12')
    root.resizable(False, False)

    game = Backgammon(root)
    tk.Button(root, text='Новая игра', font=('Arial', 12),
              command=game.restart).pack(pady=6)

    root.mainloop()


if __name__ == '__main__':
    main()