Загрузка данных
# -*- 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()