#!/usr/bin/ruby # Import the library. require 'tk' # Parameters. Width = 5 # Width of button grid. Height = 5 # Height of button grid. MinWait = 200 # Smallest button change wait (ms) MaxWait = 1400 # Largest button change wait (ms) InitWait = 800 # Initial button change wait (ms) LossRate = 2000 # Frequency to take away points. # Set defaults. Some we keep in constants to use later. BG = '#ccffcc' TkOption.add('*background', BG) TkOption.add('*activeBackground', '#ddffdd') FG = '#006600' TkOption.add('*foreground', FG) TkOption.add('*activeForeground', FG) TkOption.add('*troughColor', '#99dd99') # Root window. root = TkRoot.new('background' => BG) { title 'Click Fast' } # Button from the panel class PanelButton < TkButton private # Exchange colors on the button. def cswap for p in [['background', 'foreground'], ['activebackground', 'activeforeground']] c = cget(p[0]) configure(p[0] => cget(p[1])) configure(p[1] => c) end end public # Initialize the button within the widget sup, at position pos (zero-based) # with the number num. When pressed, send the score (+ or -) to cmd. # Scorekeeper is an object which implements an up and down methods to # receive score changes. def initialize(sup, pos, num, scorekeeper) super(sup, 'text' => num.to_s, 'command' => proc { self.pushed }, 'activeforeground' => '#990000', 'activebackground' => '#ffdddd') grid('row' => pos / Width + 1, 'column' => pos % Width, 'sticky' => 'news') @active = false @scorekeeper = scorekeeper end attr_reader :active # Activate or deactivate the button. def activate if not @active cswap @active = true end end def deactivate if @active cswap @active = false end end # When pushed, send our number, or negative our number, to the scorekeeping # command. def pushed n = self.cget('text').to_i if @active @scorekeeper.up(n) else @scorekeeper.down(n) end end end # This class calls reduces the score at the indicated time rate. class ScoreTimer # This object will call scorekeeper.down(step) each rate ms. def initialize(scorekeeper, rate = 500, step = 1) @scorekeeper = scorekeeper @rate = rate @step = step Tk.after(rate, proc { self.change }) end # Reduce the score periodically def change @scorekeeper.down(@step) Tk.after(@rate, proc { self.change }) end end # This is a box displaying a count-up timer in minutes and seconds to tenths # m:ss.d class TimeCounter < TkLabel # Initialize. Displays zero and starts the ticking event. def initialize(root) super(root, "text" => '0:00.0', 'anchor' => 'e') @count = 0 Tk.after(100, proc { self.change }) end # One clock tick (tenths of a second). Increment the counter, then build # the new display value. def change @count += 1 self.configure('text' => sprintf("%d:%02d.%d", @count / 600, (@count / 10) % 60, @count % 10)) Tk.after(100, proc { self.change }) end end # This is the main application GUI. class App private # Set the score value. def setscore(val) color = if val < 0 then 'red' else FG end @slab.configure('text' => val.to_s, 'foreground' => color) end public # The wait attribute is the amount of time (ms) between button changes. attr_writer :wait # Initialize it and have the applicate drawn in the root window. def initialize(root) # This is the label containing the score. Initially zero. @slab = TkLabel.new(root) { text "0" anchor 'e' grid('row' => 0, 'column' => 0, 'columnspan' => Width / 2, 'sticky' => 'w') } # This is the timer window at upper right. TimeCounter.new(root). grid('row' => 0, 'column' => Width/2, 'columnspan' => (Width+1)/2, 'sticky' => 'e') # Create the buttons. First, make an array of numbers from 1 to the # number of buttons, then create the buttons, each labelled with a # number chosen at random from the list, so thare are no repeats. nums = (1..Height*Width).to_a; @buts= [ ] for n in (0...Height*Width) pos = rand(nums.length) @buts.push(PanelButton.new(root, n, nums[pos], self)) nums.delete_at(pos) end # This creates the slider to adjust the speed of the game. The proc is # called whenever the slider changes, and is sent the new setting. scale = TkScale.new('command' => proc { |v| self.wait = v.to_i } ) { orient "horizontal" # Which way the slider goes. from MinWait # Value of smallest setting to MaxWait # Value of largest setting showvalue false # Don't show the numeric value of the setting. grid('row' => Height + 1, 'column' => 1, 'columnspan' => Width - 2, 'sticky' => 'news') } scale.set(InitWait) # Labels by the slider. TkLabel.new { text "Fast" anchor "w" grid("row" => Height + 1, 'column' => 0, 'sticky' => 'w') } TkLabel.new { text "Slow" anchor "e" grid("row" => Height + 1, 'column' => Width-1, 'sticky' => 'e') } @wait = InitWait # Decrement the score every LossRate period. @timer = ScoreTimer.new(self, LossRate) self.change end # Actions to increase or decrease the score. def up(delta) setscore(@slab.cget('text').to_i + delta) end def down(delta) setscore(@slab.cget('text').to_i - delta) end # Change (or set, if none is yet set) the active button. It deactivates # the button in @buts[0], It then chooses some other button at random, # activates that and swaps it into position 0. def change @buts[0].deactivate pos = rand(@buts.length - 1) + 1 @buts[0], @buts[pos] = @buts[pos], @buts[0] @buts[0].activate Tk.after(@wait, proc { self.change }) end end a = App.new(root) Tk.mainloop