Tuesday, August 20, 2019

Testing the chess clock!

In my last blog post I described a new type of chess clock, or rather a new type of time limit: the double-flag system. In short, this is a format of time control where bonus time can only be used in order to obtain a draw, not for playing for a win.

After the publication of that post, we actually tested the clock at Solrosen ("The Sunflower", a vegetarian restaurant and chess club in Gothenburg). I had written a Python hack (code below!) that allowed us to use my laptop as a chess clock. The layout is similar to the display of an ordinary digital chess clock, and to press the clock you press the keys 'P' and 'Q' respectively. In order for those keys to be clearly visible I had attached pieces of red paper on them. It's not as convenient as a real chess clock, but it works.


In order to put the clock to the test, we played with a time limit that's in reality too short for us patzers: 5 minutes each plus a bonus of 5 to 30 seconds. The "5 to 30 seconds" means (as explained in the previous blog post) that when your 5 minutes of main time have run out, you get another 30 seconds, but now your opponent has the right to claim a draw at any time (and it counts as a draw even if you checkmate your opponent on the board). When playing on the extra time, you also get a bonus "increment" of 5 seconds per move, under the constraint that your accumulated extra time will never exceed 30 seconds (details in the earlier post).

This means that in any single game, at most one player will reach the phase of extra time. Whenever the other player runs out of their main time too, the game is automatically drawn.

Normally we want to focus on the board and not the clock (this is the point of the new system!), but this time the purpose was to see if the double-flag clock makes sense in practice.

I met with my friends Anders and Bertil at Solrosen, and later some spectators joined us.

In the first game I had the black pieces against Anders, who launched the four pawns attack against my king's indian. My position was a bit cramped in the opening, but eventually I could untangle my pieces. I won an exchange when Anders missed a nasty Bd4+ Kh1 Nf2+ Rxf2 Bxf2. After that he had a difficult position with the seconds ticking away. When his five minutes had run out and he was playing on extra time, I had a minute or so left. It was still not clear if I would be able to checkmate in time. I found a queen check on the first rank, first thinking it would force a queen exchange, but as Anders realized before me, it was impossible to prevent a back-rank mate!

In the second game I was white against Bertil and chose the Tarrasch variation against his French defence. Bertil played aggressively and tried a line where he gave two minor pieces for a rook and two pawns by taking twice on c3 and forking my rooks on a1 and e1 (is this winning or sacrificing material?!). In any case it slowed down my development, but I had four minor pieces against his two in the middle-game. Eventually I got some play against his king, but at that point none of us had much time left. Bertil was the first to run out of the main five minutes, meaning I could play for a win without risk. I didn't have enough time to grind down a long endgame, but there were possibilities like the bishop sacrifice Bxh6 followed by coordinating the queen and a knight. But Bertil calmly defended. With 10 seconds or so left I thought I'd try the piece sacrifice anyway since I had nothing to lose. But then Bertil threw in a queen move threatening a perpetual check. I had to find a defensive move, and those last seconds ran out. That meant that the new possibility, draw on time, had now been realized!

In the third game Bertil played white against Anders. I was talking to some of the bystanders and didn't follow the game closely, but took a couple of photos that show Anders playing the Budapest gambit.


Eventually Bertil got the upper hand with pressure against Anders' king as well as a dangerous pawn on the queenside. Bertil could play faster and had more than two minutes when Anders went into extra time. Anders kept fighting, but 5 seconds per move isn't much in a difficult position. Finally Anders lost on time in a position that must have been very hard to defend.

After these three games we chatted with the other guys and didn't play any more that evening. But I felt that the clock had passed the first test, and several people thought it was an interesting idea!

Python code

Below is the Python code for the clock. As I said it's a hack, and it was translated from a prototype written in another language. Anyway, for what it's worth...

To try it out, even if you don't know any programming, just download a Python programming environment (for instance PyCharm) for free, check out how to create a python file, and copy-paste the code into it. And if you do know some programming, you might want to tell me how the program should have been written...

The clock has a simple layout with digital displays and six buttons for various time controls. You start it by pressing 'P' or 'Q'. It can be paused (and resumed) with the space bar.

The main time is displayed with black digits (or green, but we'll get to that in a moment), and extra time with red digits. Depending on how much time is left, it displays either hours:minutes.seconds, just minutes.seconds, or minutes.seconds.tenths.

The "Rapid", "Blitz", and "Bullet" options are quite simple, with time limits given on the buttons. There's also a "Test" option giving each player just 15 seconds, for demonstrating how the clock works.

The "Classical" option has two periods of main time (and then extra time as usual): 1h 45min for the first 40 moves, and 15 more minutes for the rest of the game. To distinguish the periods, the first one is displayed in green and the second in black. The clock doesn't count moves: When the green time is out, it just switches to the black 15 minutes (and finally to the red extra time).


There is also a "Customized" option. It's initially set to the rather serious mode of 2 hours for the first 40 moves followed by 30 minutes for the remaining game, with extra time of 30 seconds to 10 minutes. To actually customize this option, you can go into the program code, lines 208-210 in my code editor. It's in the class "Game" and under the definition of the "new_custom" method. Just below the line that says "def new_custom(self):", there are three lines that currently read:

        self.leftTime, self.rightTime = 7200, 7200
        self.A, self.b = 600, 30
        self.leftExtra, self.rightExtra = 1800, 1800

The first of these three lines determines the length of the first period of main time, in seconds (7200 seconds = 2 hours). This can be set individually for the left and right player by the way. The second line sets the initial batch of extra time and the increment per move, in this case 600 seconds (=10 minutes) and 30 seconds. As the program is designed, these are the same for both players. The third line sets the length of the (optional) second period of main time. To avoid a second period, just set it to zero.

Alright, the code. Have fun!


from tkinter import *
import time

root = Tk()
root.title("Double-Flag Chess Clock")

mainBackground = "light blue"

root.geometry("950x360+0+0")
root.configure(background=mainBackground)

leftDisplay = Label(root, font=("Typewriter", 96), bg="light yellow")
leftDisplay.place(x=50, y=50, width=400, height=150)

rightDisplay = Label(root, font=("Typewriter", 96), bg="light yellow")
rightDisplay.place(x=500, y=50, width=400, height=150)

qLabel = Label(root, font=("System", 18), anchor=W, bg=mainBackground)
qLabel.place(x=50, y=20, width=400)
qLabel.config(text='Q to press clock')

pLabel = Label(root, font=("System", 18), anchor=E, bg=mainBackground)
pLabel.place(x=500, y=20, width=400)
pLabel.config(text='P to press clock')

modeLabel = Label(root, font=("System", 18), anchor=W, bg=mainBackground)
modeLabel.place(x=50, y=200, width=400)
modeLabel.config(text="Mode: Blitz")


def reset_display():
    leftDisplay.config(fg="black")
    rightDisplay.config(fg="black")
    pLabel.config(text="P to press clock")
    qLabel.config(text="Q to press clock")


classicalString = "Classical: 1h 45min + 15min + (30s to 5min)"
rapidString = "Rapid: 15min + (10 to 60s)"
blitzString = "Blitz: 5min + (5 to 30s)"
bulletString = "Bullet: 2min + (5 to 10s)"
customString = "Customized"
testString = "Test"


def classical_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_classical()
        reset_display()
        modeLabel.config(text="Mode: Classical")
        leftDisplay.config(fg="dark green")
        rightDisplay.config(fg="dark green")


classicalButton = Button(root, text=classicalString,
                         highlightbackground=mainBackground,
                         command=classical_callback)
classicalButton.place(x=50, y=240, width=400)
classicalButton.config(bg="green")


def rapid_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_rapid()
        reset_display()
        modeLabel.config(text="Mode: Rapid")


rapidButton = Button(root, text=rapidString,
                     highlightbackground=mainBackground,
                     command=rapid_callback)
rapidButton.place(x=500, y=240, width=400)


def blitz_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_blitz()
        reset_display()
        modeLabel.config(text="Mode: Blitz")


blitzButton = Button(root, text=blitzString,
                     highlightbackground=mainBackground,
                     command=blitz_callback)
blitzButton.place(x=500, y=270, width=400)


def bullet_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_bullet()
        reset_display()
        modeLabel.config(text="Mode: Bullet")


bulletButton = Button(root, text=bulletString,
                      highlightbackground=mainBackground,
                      command=bullet_callback)
bulletButton.place(x=500, y=300, width=400)


def test_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_test()
        reset_display()
        modeLabel.config(text="Mode: Test")
        if g.leftExtra > 0:
            leftDisplay.config(fg="dark green")
            rightDisplay.config(fg="dark green")


testButton = Button(root, text=testString,
                    highlightbackground=mainBackground,
                    command=test_callback)
testButton.place(x=50, y=300, width=400)


def custom_callback():
    if g.paused or g.gameOver or (not g.leftRunning and not g.rightRunning):
        g.new_custom()
        reset_display()
        modeLabel.config(text="Mode: Customized")
        if g.leftExtra > 0:
            leftDisplay.config(fg="dark green")
            rightDisplay.config(fg="dark green")


customButton = Button(root, text=customString,
                      highlightbackground=mainBackground,
                      command=custom_callback)
customButton.place(x=50, y=270, width=400)


def enable_modes():
    classicalButton.config(state=NORMAL)
    rapidButton.config(state=NORMAL)
    blitzButton.config(state=NORMAL)
    bulletButton.config(state=NORMAL)
    testButton.config(state=NORMAL)
    customButton.config(state=NORMAL)


def disable_modes():
    classicalButton.config(state=DISABLED)
    rapidButton.config(state=DISABLED)
    blitzButton.config(state=DISABLED)
    bulletButton.config(state=DISABLED)
    testButton.config(state=DISABLED)
    customButton.config(state=DISABLED)


class Game:
    def __init__(self):
        self.leftTime, self.rightTime = 300, 300
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE
        self.A, self.b = 30, 5
        self.leftExtra, self.rightExtra = 0, 0   # Time added after 40 moves

    def new_classical(self):
        self.leftTime, self.rightTime = 6300, 6300
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE
        self.A, self.b = 60, 10
        self.leftExtra, self.rightExtra = 900, 900

    def new_rapid(self):
        self.leftTime, self.rightTime = 900, 900
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE
        self.A, self.b = 60, 10
        self.leftExtra, self.rightExtra = 0, 0

    def new_blitz(self):
        self.leftTime, self.rightTime = 300, 300
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE
        self.A, self.b = 30, 5
        self.leftExtra, self.rightExtra = 0, 0

    def new_bullet(self):
        self.leftTime, self.rightTime = 120, 120
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE
        self.A, self.b = 10, 5
        self.leftExtra, self.rightExtra = 0, 0

    def new_test(self):
        self.leftTime, self.rightTime = 15, 15
        self.A, self.b = 15, 5
        self.leftExtra, self.rightExtra = 0, 0
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE

    def new_custom(self):
        self.leftTime, self.rightTime = 7200, 7200
        self.A, self.b = 600, 30
        self.leftExtra, self.rightExtra = 1800, 1800
        self.reference = 0
        self.leftRunning, self.rightRunning, self.paused = FALSE, FALSE, FALSE
        self.nLeftFallen, self.nRightFallen = 0, 0
        self.gameOver = FALSE


g = Game()


def key_pressed(event):

    if event.char == 'p' and not g.paused and not g.leftRunning and not g.gameOver:
        disable_modes()
        g.rightRunning = FALSE
        pLabel.configure(text="")
        if g.nRightFallen == 1:
            if g.rightTime > g.A - 2*g.b:
                g.rightTime = (g.A + g.rightTime) / 2
            else:
                g.rightTime += g.b
        g.reference = g.leftTime + time.time()
        g.leftRunning = TRUE
        qLabel.configure(text="Q to press clock")

    if event.char == 'q' and not g.paused and not g.rightRunning and not g.gameOver:
        disable_modes()
        g.leftRunning = FALSE
        qLabel.configure(text="")
        if g.nLeftFallen == 1:
            if g.leftTime > g.A - 2*g.b:
                g.leftTime = (g.A + g.leftTime) / 2
            else:
                g.leftTime += g.b
        g.reference = g.rightTime + time.time()
        g.rightRunning = TRUE
        pLabel.configure(text="P to press clock")

    if event.char == ' ' and (g.leftRunning or g.rightRunning):
        g.paused = not g.paused
        if g.paused:
            enable_modes()
        else:
            disable_modes()
        if not g.paused and g.leftRunning:
            qLabel.configure(text="Q to press clock")
            g.reference = g.leftTime + time.time()
        if not g.paused and g.rightRunning:
            pLabel.configure(text="P to press clock")
            g.reference = g.rightTime + time.time()
        if g.paused and g.leftRunning:
            qLabel.configure(text="Paused")
        if g.paused and g.rightRunning:
            pLabel.configure(text="Paused")


root.bind("<Key>", key_pressed)


def update_clock():
    if g.leftRunning and not g.paused and g.nLeftFallen == 0:
        g.leftTime = g.reference - time.time()
        if g.leftTime < 0:
            if g.leftExtra > 0:
                g.leftTime = g.leftExtra
                g.leftExtra = 0
                leftDisplay.configure(fg="black")
            else:
                g.leftTime = g.A
                g.nLeftFallen = 1
                leftDisplay.configure(fg="red")
                if g.nLeftFallen + g.nRightFallen >= 2:
                    g.gameOver = TRUE
                    g.leftRunning = FALSE
                    g.rightRunning = FALSE
                    qLabel.config(text="1/2")
                    pLabel.config(text="1/2")
                    enable_modes()
            g.reference = g.leftTime + time.time()

    if g.leftRunning and not g.paused and g.nLeftFallen == 1:
        g.leftTime = g.reference - time.time()
        if g.leftTime < 0:
            g.leftTime = 0
            g.nLeftFallen = 2
            g.gameOver = TRUE
            g.leftRunning = FALSE
            g.rightRunning = FALSE
            qLabel.config(text="0")
            pLabel.config(text="1")
            enable_modes()

    if g.rightRunning and not g.paused and g.nRightFallen == 0:
        g.rightTime = g.reference - time.time()
        if g.rightTime < 0:
            if g.rightExtra > 0:
                g.rightTime = g.rightExtra
                g.rightExtra = 0
                rightDisplay.configure(fg="black")
            else:
                g.nRightFallen = 1
                g.rightTime = g.A
                rightDisplay.configure(fg="red")
                if g.nLeftFallen + g.nRightFallen >= 2:
                    g.gameOver = TRUE
                    g.leftRunning = FALSE
                    g.rightRunning = FALSE
                    qLabel.config(text="1/2")
                    pLabel.config(text="1/2")
                    enable_modes()
            g.reference = g.rightTime + time.time()

    if g.rightRunning and not g.paused and g.nRightFallen == 1:
        g.rightTime = g.reference - time.time()
        if g.rightTime < 0:
            g.rightTime = 0
            g.nRightFallen = 2
            g.gameOver = TRUE
            g.leftRunning = FALSE
            g.rightRunning = FALSE
            qLabel.config(text="1")
            pLabel.config(text="0")
            enable_modes()
    lh = int(g.leftTime / 3600)
    lm = int(g.leftTime / 60) % 60
    ls = int(g.leftTime) % 60
    lt = int(g.leftTime * 10) % 10
    rh = int(g.rightTime / 3600)
    rm = int(g.rightTime / 60) % 60
    rs = int(g.rightTime) % 60
    rt = int(g.rightTime * 10) % 10
    if lh > 0:
        leftDisplay.config(text=f'{lh:01}' + ':' + f'{lm:02}' + '.' + f'{ls:02}')
    elif lm > 0:
        leftDisplay.config(text=f'{lm:02}' + '.' + f'{ls:02}')
    else:
        leftDisplay.config(text=f'{lm:02}' + '.' + f'{ls:02}' + '.' + f'{lt:01}')
    if rh > 0:
        rightDisplay.config(text=f'{rh:01}' + ':' + f'{rm:02}' + '.' + f'{rs:02}')
    elif rm > 0:
        rightDisplay.config(text=f'{rm:02}' + '.' + f'{rs:02}')
    else:
        rightDisplay.config(text=f'{rm:02}' + '.' + f'{rs:02}' + '.' + f'{rt:01}')
    root.after(20, update_clock)  # run itself after given number of milliseconds


# run first time
update_clock()

root.mainloop()