Logo Search packages:      
Sourcecode: gameclock version File versions  Download package

gameclock.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# gameclock - a simple chess/game clock
# Copyright (C) 2008 Antoine Beaupré <anarcat@koumbit.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pygtk
pygtk.require('2.0')
import pango
import gtk
import gobject
import time
import math
import getopt
import sys

version = "2.0"
verbose = 0 # 0 means not verbose, higher numbers show more information, see maybe_print for more info
sec_loop_timeout = 200 # in ms, clocks are incremented every time this timeout ends
ms_loop_timeout = 50 # in ms, the same as above, in milisecond mode
default_time = 2 * 60 * 1000 # in ms
default_fisher = 10 # in seconds, default is used only by the UI
default_players = 2 # default number of players

00038 class Clock:
    """The main clock engine

    Each clock is an instance of this class. This could be considered
    like a merge between a controler and a model in an MVC
    model. However, this software isn't based on the MVC model in any
    remote way.
    
    This should be pretty precise and doesn't lag from empirical
    observations (watching the clocks compared to the Gnome clock
    applet)
    """
    time = default_time
    last_start = 0 # 0 if stopped
    text = "" # rendered version of the clock
    dead = False
    game = None
    # a clock is part of a chained list
    next = None

00058     def __init__(self, game):
        """Setup the clock engine
        
        The clock is stopped by default and the display is refreshed
        """
        self.dead = self.time < 0 # calculated only after update() and should be used only this way, it is a cache
        self.game = game
        self.update()

00067     def start(self):
        """Start the timer
        
        This marks the new timestamp and sets the background color
        """
        self.last_start = time.time()
        # no need to update, time shouldn't have changed since last stop

00075     def stop(self):
        """Stop the timer
        
        This computes the new cumulatative time based on the start
        timestamp. It resets the timestamp to zero to mark the timer as
        stopped.
        
        This also resets the event box's color and triggers an update of
        the label.
        
        XXX: Note that this function takes *some* time to process. This
        time is lost and not given to any participant. Maybe that should
        be fixed for the clock to be really precised, by compensation
        for the duration of the function.
        
        Another solution would be to create a thread for the Game engine
        """
        if self.last_start:
            self.time = self.get_time()
            self.last_start = 0
            if self.game.fisher:
                self.time += self.game.fisher
        self.update()

00099     def pause(self):
        """pause/unpause the timer
        
        this will start the timeer if stopped and stop it if started
        """
        if self.last_start:
            self.time = self.get_time()
            self.last_start = 0          
            self.update()
        else:
            self.start()

00111     def get_time(self):
        """return the current time of the clock in ms"""
        if self.last_start:
            diff = time.time() - self.last_start
        else:
            diff = 0
        return self.time - diff*1000

00119     def update(self):
        """Refresh the display of the clock's widget"""
        # convert to seconds because that's what gmtime wants
        miliseconds = self.get_time()
        secs = miliseconds/1000
        self.text = time.strftime("%H:%M:%S", time.gmtime(abs(secs)))
        days = abs(secs) / ( 24 * 60 * 60 )
        if days >= 1:
            self.text = "%dd " % days + self.text
        self.dead = secs < 0
        if self.dead:
            self.text = "-" + self.text
        if self.game.miliseconds:
            mantissa = abs(miliseconds) - (math.floor(abs(secs))*1000)
            self.text += ".%03d" % (mantissa)
        return self.text

00136     def __str__(self):
        """make a better string representation of the objects
        
        we basically dump all variables and some functions
        """
        return "  clock engine %s time: %d last: %d diff: %f dead: %d text: %s\n  next %s" % ( object.__str__(self), self.time, self.last_start, time.time() - self.last_start, self.dead, self.text, self.next)

00143 class Game:
    """the game engine
    
    this regroups two clocks and handles turn switches
    """

    # the number of turns played
    turns = 0
    fisher = 0 # 0 if fisher disabled, otherwise in ms
    miliseconds = False
    mode = 'blitz'
    
    # the clocks in the game
    #
    # in chess there are two clocks, but there can be more. this is simply a list
    cur_clock = None
    first_clock = None

    def __init__(self, clocks, miliseconds, fisher):
        self.miliseconds = miliseconds
        self.fisher = fisher

        # the clock engines
        self.cur_clock = self.first_clock = p = Clock(self)
        for i in range(clocks-1):
            p.next = Clock(self)
            p = p.next

00171     def start(self):
        """start the game
        
        this basically starts the clock
        """
        self.cur_clock.start()
        self.cur_clock.update()

00179     def end_turn(self):
        """end the turn
        
        this is the handler for the main button. it will stop the
        active clock and start the other and switch the active clock
        """
        self.cur_clock.stop()
        # XXX: we might lose a few ms here
        self.cur_clock = self.next_clock()
        self.cur_clock.start()
        self.turns += 1

    def next_clock(self):
        return self.cur_clock.next or self.first_clock

00194     def pause(self):
        """pause the game
        
        this just pauses the current clock
        """
        self.cur_clock.pause()

00201     def switch_clock(self):
        """change the current clock"""
        self.cur_clock = self.next_clock()

00205     def get_turns(self):
        """the number of complete turns played
        
        a 'complete turn' is a turn played by each player

        this is 1-based, i.e. when the game starts, it's turn 1, after
        every player has played once, it's turn 2, etc.
        """
        return math.floor(self.turns/self.count_players()) + 1

    def count_players(self):
        p = self.first_clock
        count = 0
        while p:
            count += 1
            p = p.next
        return count

00223     def __str__(self):
        """make a better string representation of the objects
        
        we basically dump all variables and some functions
        """
        return "  game engine %s\n  turns: %f / %d fisher: %d miliseconds: %d\n  first %s\n  current %s" % ( object.__str__(self), self.turns, self.get_turns(), self.fisher, self.miliseconds, self.first_clock, self.cur_clock)

00230 class GameclockGtkUI:
    """this class handles most of the UI and turned-based logic
    
    It is not designed for UI-abstraction yet, but could be, if the
    turn-based logic is ripped in a seperate class
    """

    clock_evbox_cnt = 0

00239     class ClockUI:
        """this class is used to encapsulate the various controls related with a clock"""

        # like the game Clock, it is a linked list
        next = None

        def __init__(self, ui, clock = None):
            self.next = clock
            self.label = gtk.Label()
            self.label.modify_font(pango.FontDescription("sans 72"))
            self.label.show()

            # event boxes to be able to color the labels
            evbox = gtk.EventBox()
            evbox.modify_bg(gtk.STATE_NORMAL, evbox.get_colormap().alloc_color("green"))
            evbox.show()
            evbox.add(self.label)
            ui.add_clock_evbox(evbox)

        def __del__(self):
            self.label.parent.destroy()
            self.label.destroy()
            del self.label

00263     def main(self):
        """simple stub to fire up the main gtk event loop"""
        gtk.main()

    def quit(self, widget, a = None, b = None, c = None):
        gtk.main_quit()

    def add_clock_evbox(self, evbox):
        self.clock_evbox_cnt += 1
        cols = 2 # all the time
        rows = (self.clock_evbox_cnt - 1)/ cols + 1
        self.clock_table.resize(rows, cols)
        l = ( self.clock_evbox_cnt % cols + 1 ) % cols
        r = l + 1
        t = rows - 1
        b = rows
        self.clock_table.attach(evbox, l, r, t, b)
    
    def __init__(self, fullscreen = False):
        """create the main user interface with GTK"""
        self.loop_timeout = sec_loop_timeout
        self.fullscreen = fullscreen

        # create a new window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)

        # handle window close events
        self.window.connect("delete_event", lambda a, b: False)    
        self.window.connect("destroy", self.quit)
    
        # Sets the border width of the window.
        self.window.set_border_width(10)

        # the key shortcuts, space is bound to the button, below
        accel_group = gtk.AccelGroup()
        accel_group.connect_group(ord('q'), gtk.gdk.CONTROL_MASK, 0, self.quit)
        accel_group.connect_group(ord('p'), 0, 0, self.handle_pause)
        accel_group.connect_group(ord('r'), gtk.gdk.CONTROL_MASK, 0, self.handle_reset)
        accel_group.connect_group(ord('f'), 0, 0, self.handle_fullscreen)
        self.window.add_accel_group(accel_group)

        self.window.connect('key_press_event', self.handle_key_press)

        # main window consists of a vbox containing two hbox
        vlayout = gtk.VBox(False, 0)
        self.window.add(vlayout)

        # turn counter
        self.turns = gtk.Label()
        vlayout.pack_start(self.turns, False, False, 0)

        # the clocks
        self.clock_table = gtk.Table(1, 2, True)
        self.clock_table.show()
        vlayout.pack_start(self.clock_table, True, True, 0)

        try:
            players = int(self.players_val.get_value())
        except:
            players = default_players

        # the clock engines
        p = self.cur_clock = self.first_clock = GameclockGtkUI.ClockUI(self)
        for i in range(players-1):
            p.next = GameclockGtkUI.ClockUI(self)
            p = p.next

        self.setup_game()

        # all controls is regular controls + start/stop
        all_controls = gtk.VBox(False, 0)
        all_controls.show()
        self.controls = gtk.HBox(False, 0)
        self.controls.show()
        vlayout.pack_start(all_controls, False, False, 0)

        # the widgets to change the starting time
        (hours, minutes, seconds) = map(int, time.strftime("%H:%M:%S", time.gmtime(default_time/1000)).split(":"))
        # XXX: spinbuttons limited to 24h-1s because of limitations of strftime
        self.hours_val = gtk.Adjustment(hours, 0, 23, 1, 10, 0)
        self.minutes_val = gtk.Adjustment(minutes, 0, 59, 1, 10, 0)
        self.seconds_val = gtk.Adjustment(seconds, 0, 59, 1, 10, 0)
        self.hours_val.connect("value_changed", self.handle_clock_change)
        self.minutes_val.connect("value_changed", self.handle_clock_change)
        self.seconds_val.connect("value_changed", self.handle_clock_change)
        hours_val_btn = gtk.SpinButton(self.hours_val, 0.0, 0)
        minutes_val_btn = gtk.SpinButton(self.minutes_val, 0.0, 0)
        seconds_val_btn = gtk.SpinButton(self.seconds_val, 0.0, 0)
        hours_val_btn.show()
        minutes_val_btn.show()
        seconds_val_btn.show()

        clock_controls = gtk.HBox(False, 0)
        clock_controls.pack_start(hours_val_btn, False, False, 10)
        clock_controls.pack_start(minutes_val_btn, False, False, 10)
        clock_controls.pack_start(seconds_val_btn, False, False, 10)
        clock_controls.show()

        clock_controls_box = gtk.VBox(False, 0)
        label = gtk.Label("Time limit")
        label.show()

        clock_controls_box.pack_start(label, False, False, 10)
        clock_controls_box.pack_start(clock_controls, False, False, 10)
        clock_controls_box.show()

        self.controls.pack_start(clock_controls_box)

        # the main toggle button, used by various signal handlers
        self.startstop = gtk.Button("Start (space)")
        self.startstop.add_accelerator("activate", accel_group, ord(' '), 0, 0)
        # we keep the signal id to turn it off on first click
        self.start_signal = self.startstop.connect("clicked", self.handle_start_game, None)
        self.startstop.show()
        all_controls.pack_start(self.startstop, False, False, 0)
        # now we can pack the other controls
        all_controls.pack_start(self.controls, True, True, 0)

        # The game mode radio buttons
        type_layout = gtk.VBox(False, 10)
        type_layout.set_border_width(10)
        type_layout.show()

        label = gtk.Label("Game type")
        label.show()
        type_layout.pack_start(label, True, True, 0)

        button = gtk.RadioButton(None, "Blitz")
        button.connect("toggled", self.handle_blitz, "Blitz")
        button.set_active(True)
        type_layout.pack_start(button, True, True, 0)
        button.show()
      
        fisher_layout = gtk.HBox(False, 10)
        type_layout.pack_start(fisher_layout, True, True, 0)

        button = gtk.RadioButton(button, "Fisher")
        button.connect("toggled", self.handle_fisher, "Fisher")
        fisher_layout.pack_start(button, True, True, 0)
        button.show()

        self.fisher_label = gtk.Label('Delay: ')
        fisher_layout.pack_start(self.fisher_label, True, False, 0)

        self.fisher_val = gtk.Adjustment(default_fisher, 1, 10000, 1, 10, 0)
        self.fisher_val_btn = gtk.SpinButton(self.fisher_val, 0.0, 0)
        self.fisher_val.connect("value_changed", self.handle_fisher_change)
        fisher_layout.pack_start(self.fisher_val_btn, False, False, 0)
        fisher_layout.show()

        self.controls.pack_start(type_layout)

        misc_box = gtk.VBox(False, 0)
        misc_box.show()

        ms_ctrl = gtk.CheckButton("Display _miliseconds")
        ms_ctrl.connect('toggled', self.handle_miliseconds)
        ms_ctrl.show()
        misc_box.pack_start(ms_ctrl)

        label = gtk.Label('Number of players: ')
        label.show()
        misc_box.pack_start(label, False, False, 10)

        self.players_val = gtk.Adjustment(default_players, 1, 10000, 1, 10, 0)
        players_val_btn = gtk.SpinButton(self.players_val, 0.0, 0)
        self.players_val.connect("value_changed", self.handle_players_change)
        players_val_btn.show()
        misc_box.pack_start(players_val_btn, False, False, 0)

        self.controls.pack_start(misc_box, False, False, 0)

        # the main toggle button, used by various signal handlers
        starter = gtk.Button("Toggle starting player")
        starter.connect("clicked", self.handle_switch_clock)
        starter.show()
        self.controls.pack_start(starter, True, True, 0)

        # little help
        label = gtk.Label("<b>shift</b> and <b>space</b> keys end turns; <b>p</b> pauses; <b>control-q</b> quits; <b>control-r</b> resets the game; <b>f</b> enables fullscreen mode")
        label.set_use_markup(True)
        label.show()
        vlayout.pack_start(label, False, False, 0)
        vlayout.show()
    
        if self.fullscreen:
            self.window.fullscreen()
        self.window.show()

    def maybe_print(self, text):
        if verbose:
            t = ""
            state = ""
            if verbose > 1:
                t = "[%f] " % time.time()
                if verbose > 2:
                    state = "\n  game engine state: %s" % self.game
            print t + text + state

    def refresh(self):
        if self.game.miliseconds:
            font = "sans 48"
        else:
            font = "sans 72"

        p = self.first_clock
        q = self.game.first_clock
        while p:
            p.label.modify_font(pango.FontDescription(font))
            p.label.set_label(q.text)
            p = p.next
            q = q.next
        
        self.turns.set_label("Turn %d" % self.game.get_turns())
        self.hilight()
        
    def refresh_current(self):
        """refresh the active clock
        
        this handler is ran periodically through a timeout signal to make sure that the current clock is updated
        """
        self.cur_clock.label.set_label(self.game.cur_clock.update())
        active = self.cur_clock.label.get_parent()
        if self.game.cur_clock.dead:
            active.modify_bg(gtk.STATE_NORMAL, active.get_colormap().alloc_color("red"))
        return True

    def hilight(self):
        """hilight the proper clocks with proper colors
        
        this is 'transparent' for the inactive clock and colored for the
        active clock. the color depends on wether the clock is 'dead' or
        not
        """
        p = self.first_clock
        q = self.game.first_clock
        while p:
            evbox = p.label.get_parent()
            if p == self.cur_clock:
                if q.dead:
                    color = "red"
                else:
                    color = "green"
                color = evbox.get_colormap().alloc_color(color)
            else:
                color = None
            evbox.modify_bg(gtk.STATE_NORMAL, color)
            p = p.next
            q = q.next

    def setup_game(self, miliseconds = False, fisher = 0):
        """initial setup of the clocks, game and labels"""
        try:
            players = int(self.players_val.get_value())
        except:
            players = default_players

        # the game
        self.game = Game(players, miliseconds, fisher)
        self.maybe_print("game setup with %d players" % players)
        self.refresh()

    def handle_key_press(self, widgets, event):
        keyname = gtk.gdk.keyval_name(event.keyval)
        if keyname == 'Shift_L':
            if self.game.cur_clock == self.game.first_clock:
                self.handle_end_turn()
        elif keyname == 'Shift_R':
            if self.game.cur_clock != self.game.first_clock:
                self.handle_end_turn()
        elif keyname == 'Escape':
            self.handle_reset()
        self.maybe_print("key %s (%d) was pressed" % (keyname, event.keyval))

    def handle_fullscreen(self, widget, event, a, b):
        if self.fullscreen:
            self.window.unfullscreen()
        else:
            self.window.fullscreen()
        self.fullscreen = not self.fullscreen

    # XXX: those could be simplified by tagging the radio with the
    # mode, which would be passed along
    def handle_blitz(self, widget, data=None):
        if widget.get_active():
            self.game.mode = 'blitz'
            self.game.fisher = 0
            self.fisher_val_btn.hide()
            self.fisher_label.hide()
            self.maybe_print("using blitz mode")
        return

    def handle_fisher(self, widget, data=None):
        if widget.get_active():
            self.game.mode = 'fisher'
            self.handle_fisher_change(self.fisher_val)
            self.fisher_val_btn.show()
            self.fisher_label.show()
            self.maybe_print("using fisher mode")
        return

    def handle_fisher_change(self, widget, data=None):
        self.game.fisher = int(widget.get_value() * 1000)
            
    def handle_players_change(self, widget, data=None):
        p = self.first_clock
        q = self.game.first_clock
        count = 1
        players = int(widget.get_value())
        # reach the last node
        while count < players and p.next:
            self.maybe_print("count: %d, players: %d" % (count, players))
            p = p.next
            q = q.next
            count += 1
        while count == players and p.next:
            self.maybe_print("too many clocks, killing a clock")
            tmp = p.next
            p.next = p.next.next
            del tmp
            tmp = q.next
            q.next = q.next.next
            del tmp
        if p.next:
            p = p.next
            q = q.next
            count += 1
        self.maybe_print("count: %d, players: %d" % (count, players))
        while count < players:
            self.maybe_print("adding a new clock")
            p.next = GameclockGtkUI.ClockUI(self)
            q.next = Clock(self.game)
            # XXX: this would be cleaner if a copy was implemented in the Clock object
            q.next.time = q.time
            q.next.update()
            p = p.next
            q = q.next
            count += 1
        self.refresh()
        self.maybe_print("now at %d clocks" % count)

    def handle_end_turn(self, widget = None, data=None):
        """handle end turn events
        
        this passes the message to the gaming engine as quickly as
        possible then goes around updating the UI
        """
        if not self.first_clock.next:
            self.game.pause()
        else:
            self.game.end_turn()
        # update the current clock pointer
        self.cur_clock = self.cur_clock.next
        if not self.cur_clock:
            self.cur_clock = self.first_clock

        # some reason it doesn't work to just update the old clock label, we need to update both
        self.refresh()
        self.hilight()
        self.maybe_print("ended turn")
        
    def handle_switch_clock(self, widget):
        """change the current clock
        
        simply switch the clock in the game engine and rehilight
        """
        self.game.switch_clock()
        self.cur_clock = self.cur_clock.next
        if not self.cur_clock:
            self.cur_clock = self.first_clock
        self.hilight()
        self.maybe_print("changing starting clock")

    def handle_start_game(self, widget, data=None):
        """start the game
        
        this is the handler for the main button when the game has not
        been started yet
        
        it will change the button to an "end turn" button and connect
        the 'end_turn' function to it
        
        it will also start the game engine itself
        """
        self.timeout_source = gobject.timeout_add(self.loop_timeout, self.refresh_current)
        self.startstop.handler_block(self.start_signal)
        self.end_turn_signal = self.startstop.connect("clicked", self.handle_end_turn, None)

        if self.game.count_players() == 1:
            self.startstop.set_label('Pause timer (space)')
        else:
            self.startstop.set_label('End turn (space)')

        self.game.start()
        self.turns.set_label("Turn %d" % self.game.get_turns())
        self.turns.show()
        self.controls.hide()
        self.maybe_print("starting game, refresh rate %dms" % self.loop_timeout)

    def handle_pause(self, widget, data=None, a=None, b=None):
        """pause handler
        
        just a stub for the game engine for now
        """
        self.maybe_print("pausing game")
        self.game.pause()

    def handle_reset(self, widget=None, data=None, a=None, b=None):
        """game reset handler

        this is a bit more complicated:
        * destroy and recreate the game engine
        * remove the auto-refresh and reconnect the right signals
        * fix the main label
        * show the controls and fix the hilight
        """
        self.maybe_print("reseting game")
        ms = self.game.miliseconds
        fisher = self.game.fisher
        del self.game
        self.setup_game(ms, fisher)
        self.cur_clock = self.first_clock
        try:
            gobject.source_remove(self.timeout_source) # stop the refresh
            self.startstop.handler_block(self.end_turn_signal)
        except AttributeError:
            pass
        else:
            self.start_signal = self.startstop.connect("clicked", self.handle_start_game, None)
        self.startstop.set_label('Start (space)')
        self.handle_clock_change() # refresh the labels and time counters based on the chosen times
        self.controls.show()
        self.hilight()

    def handle_clock_change(self, widget = None):
        hours = self.hours_val.get_value()
        minutes = self.minutes_val.get_value()
        seconds = self.seconds_val.get_value()

        # XXX: note that we edit all clocks at the same time
        p = self.game.first_clock
        while p:
            p.time = ( hours * 60 * 60 ) + ( minutes * 60 ) + seconds
            p.time *= 1000
            p.update()
            p = p.next
        self.refresh()

    def handle_miliseconds(self, widget):
        if widget.get_active():
            self.loop_timeout = ms_loop_timeout
        else:
            self.loop_timeout = sec_loop_timeout

        self.game.miliseconds = widget.get_active()
        p = self.game.first_clock
        while p:
            p.update()
            p = p.next
        self.refresh()
        self.maybe_print("activating miliseconds display")

def usage():
    print """gameclock v%s  Copyright (C) 2008  Antoine Beaupré
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions.
For details see the COPYRIGHT file distributed along this program.

Usage:
  %s [ -h | -v ... | -f ]

  -h --help: display this help
  -v --verbose: display progress information to stdout. repeating the flag will show more information
  -f --fullscreen: start in fullscreen mode
  -t --time <n>: start time in seconds
  -p --players <n>: start n clocks

See the manpage for more information.""" % (version, sys.argv[0])

if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hvft:p:", ["help", "verbose", "fullscreen", "time", "players"])
    except getopt.GetoptError, err:
        # print help information and exit:
        usage()
        print "\nError: " + str(err) # will print something like "option -a not recognized"
        sys.exit(2)

    fullscreen = False
    for o, a in opts:
        if o in ("-v", "--verbose"):
            verbose += 1
        elif o in ("-h", "--help"):
            usage()
            sys.exit()
        elif o in ("-f", "--fullscreen"):
            fullscreen = True
        elif o in ("-t", "--time"):
            default_time = int(a) * 1000
        elif o in ("-p", "--players"):
            default_players = int(a)
        else:
            assert False, "unhandled option"

    clock = GameclockGtkUI(fullscreen)
    clock.main()

Generated by  Doxygen 1.6.0   Back to index