Playing Squash with the Wii Remote

Implementing a Wii game with Ruby

I gave a presentation about developing a Squash game using the Nintendo Wii Remote at the Sheffield Ruby User Group (ShRUG).

Here is the source code of the main program

#!/usr/bin/env ruby
require 'rubygems'
require 'hornetseye_rmagick'
require 'hornetseye_alsa'
require 'opengl'
require 'cwiid'
include Hornetseye
WIDTH = 800
HEIGHT = 600
GRAVITY = 9.81
SPEED_PER_VOLUME = 16.0
SIZE_X = 3.2
SIZE_Z = 9.75
BAR_HEIGHT = 0.43
BAR_THICKNESS = 0.1
RADIUS = 0.02025 * 2
REFLECTION = 0.7
AIR_FRICTION = 0.00
ROLL_FRICTION = 0.1
MIN_SPEED = 0.8
ACC_RISING = 20.0
ACC_FALLING = 0.0
MIN_DELAY = 0.3
MIN_HEIGHT = 0.15
OBSERVER_Y = -2.4
OBSERVER_Z = -10.5
DIST_Z = 6.0
X0 = -2.0
H0 = 1.5
SERVE_SPEED = 5.0
V_MIN = 8.0
V_MAX = 20.0
Z0 = DIST_Z - SIZE_Z
NORM_Z = -1.5
L = 1.0
# switch on lights with WiiMote
# http://www.paulsprojects.net/opengl/shadowmap/shadowmap.html
# http://bitwiseor.com/gl_arb_shadow/3/
puts 'Put Wiimote in discoverable mode now (press 1+2)...'
wiimote = nil
wiimote = WiiMote.new
wiimote.rpt_mode = WiiMote::RPT_BTN | WiiMote::RPT_ACC if wiimote
$floor = MultiArray.load_ubytergb 'floor.png'
$side = MultiArray.load_ubytergb 'side.png'
$back = MultiArray.load_ubytergb 'back.png'
( MultiArray( SINT, 2, 16 ).new * 0.5 ).to_sint
s = File.new( 'wall.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$wall = MultiArray( SINT, 2, m.size / 4 ).new m
s = File.new( 'ground.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$ground = MultiArray( SINT, 2, m.size / 4 ).new m
s = File.new( 'racket.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$racket = MultiArray( SINT, 2, m.size / 4 ).new m
$pos = [ X0, RADIUS, Z0 ]
$v = [ 0.0, 0.0, 0.0 ]
$t = Time.new.to_f
$sign = nil
$strength = 0.0
$delay = Time.new.to_f
$alsa = AlsaOutput.new 'default:0'
$sounds = []
def init
  GL.ClearColor 0.0, 0.0, 0.0, 1.0
  GL.Lightfv GL::LIGHT0, GL::AMBIENT, [ 1.0, 1.0, 1.0, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::DIFFUSE, [ 1.0, 1.0, 1.0, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::POSITION, [ 0.0, 6.5 + OBSERVER_Y, -3.0 + OBSERVER_Z, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::SPOT_DIRECTION, [ 0.0, -1.0, -0.5 ]
  GL.Lightf GL::LIGHT0, GL::SPOT_CUTOFF, 60.0
  GL.Lightf GL::LIGHT0, GL::SPOT_EXPONENT, 1.2
  GL.Enable GL::LIGHT0
  GL.Enable GL::LIGHTING
  GL.DepthFunc GL::LESS
  GL.Enable GL::DEPTH_TEST
  $tex = GL.GenTextures 3
  GL.BindTexture GL::TEXTURE_2D, $tex[0]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $floor.memory.export
  GL.BindTexture GL::TEXTURE_2D, $tex[1]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $side.memory.export
  GL.BindTexture GL::TEXTURE_2D, $tex[2]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $back.memory.export
  $list = GL.GenLists 2
  GL.NewList $list, GL::COMPILE
  GL.Enable GL::TEXTURE_2D
  GL.Material GL::FRONT, GL::AMBIENT, [ 0.2, 0.2, 0.2, 1.0 ]
  GL.Material GL::FRONT, GL::DIFFUSE, [ 0.8, 0.8, 0.8, 1.0 ]
  GL.BindTexture GL::TEXTURE_2D, $tex[0]
  GL.Begin GL::QUADS
  GL.Normal 0.0, 1.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0, 0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, 0.0
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 0.0, -SIZE_Z
  GL.End
  GL.BindTexture GL::TEXTURE_2D, $tex[2]
  GL.Begin GL::QUADS
  GL.Normal 0.0, 0.0, 1.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 5.0, -SIZE_Z
  GL.Normal 0.0, 1.0, 0.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.Normal 0.0, 0.0, 1.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0, BAR_THICKNESS - SIZE_Z
  GL.End
  GL.BindTexture GL::TEXTURE_2D, $tex[1]
  GL.Begin GL::QUADS
  GL.Normal 1.0, 0.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0,  0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex -SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex -SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 5.0,  0.0
  GL.Normal -1.0, 0.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex  SIZE_X, 0.0,  0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex  SIZE_X, 5.0,  0.0
  GL.End
  GL.Disable GL::TEXTURE_2D
  GL.EndList
  GL.NewList $list + 1, GL::COMPILE
  GL.Material GL::FRONT, GL::AMBIENT, [ 0.7, 0.7, 0.0, 1.0 ]
  GL.Material GL::FRONT, GL::DIFFUSE, [ 0.3, 0.3, 0.0, 1.0 ]
  GLUT.SolidSphere RADIUS, 16, 16
  GL.EndList
end
display = proc do
  GL.Clear GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT
  GL.CallList $list
  GL.PushMatrix
  GL.Translate *$pos
  GL.CallList $list + 1
  GL.PopMatrix
  GLUT.SwapBuffers
end
reshape = proc do |w, h|
  GL.Viewport 0, 0, w, h
  GL.MatrixMode GL::PROJECTION
  GL.LoadIdentity
  GLU.Perspective 25.0, w.to_f/h, 1.0, 25.0
  GL.MatrixMode GL::MODELVIEW
  GL.LoadIdentity
  GL.Translate 0.0, OBSERVER_Y, OBSERVER_Z
end
keyboard = proc do |key, x, y|
  case key
  when ?\e
    exit 0
  when ?s
    $pos = [ X0, H0, Z0 ]
    $v = [ 0.0, SERVE_SPEED, 0.0 ]
  when ?\ 
    vz = V_MAX / 2
    t = ( SIZE_Z + $pos[2] + DIST_Z / REFLECTION ) / vz
    vy = 0.5 * GRAVITY * t - $pos[1] / t
    vx = -2 * $pos[0] / t
    $v = [ vx, vy, -vz ]
    #vz = 12.0
    #t = ( DIST_Z + DIST_Z / REFLECTION ) / vz
    #vy = 0.5 * GRAVITY * t - H0 / t
    #vx = -2 * X0 / t
    #$v = [ vx, vy, -vz ]
    #$pos = [ X0, H0, Z0 ]
    $sounds.push( ( $racket * [ 0.2, 0.2 ].min ).to_sint )
  end
end
animate = proc do
  dt = Time.new.to_f - $t
  $t += dt
  g = $pos[1] > RADIUS ? GRAVITY : 0
  $pos[0] += $v[0] * dt
  $pos[1] += $v[1] * dt - 0.5 * g * dt ** 2
  $pos[2] += $v[2] * dt
  v = Math.sqrt $v.inject( 0.0 ) { |a,b| a + b ** 2 }
  if g > 0 or v > MIN_SPEED
    f = g > 0 ? AIR_FRICTION : ROLL_FRICTION
    r = f * v
    $v[0] -= $v[0] * r * dt
    $v[1] -= $v[1] * r * dt + g * dt
    $v[2] -= $v[2] * r * dt
  else
    $v = [ 0, 0, 0 ]
  end
  if $pos[0] < -SIZE_X + RADIUS
    $pos[0] = 2 * ( -SIZE_X + RADIUS ) - $pos[0]
    $v[0] = -$v[0] * REFLECTION
    $sounds.push( ( $wall * [ $v[0].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[0] > SIZE_X - RADIUS
    $pos[0] = 2 * ( SIZE_X - RADIUS ) - $pos[0]
    $v[0] = -$v[0] * REFLECTION
    $sounds.push( ( $wall * [ $v[0].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[1] < RADIUS
    if $v[1] < -MIN_SPEED
      $pos[1] = 2 * RADIUS - $pos[1]
      $v[1] = -$v[1] * REFLECTION
      $sounds.push( ( $ground * [ 0.3 * $v[1].abs / SPEED_PER_VOLUME, 0.3 ].min ).to_sint )
    else
      $pos[1] = RADIUS
      $v[1] = 0
    end
  end
  b = $pos[1] > BAR_HEIGHT ? -SIZE_Z + RADIUS : -SIZE_Z + RADIUS + BAR_THICKNESS
  if $pos[2] < b and $v[2] < 0
    $pos[2] = 2 * b - $pos[2]
    $v[2] = -$v[2] * REFLECTION
    $sounds.push( ( $wall * [ $v[2].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[2] > -RADIUS
    $pos = [ X0, RADIUS, Z0 ]
    $v = [ 0, 0, 0 ]
  end
  if wiimote
    wiimote.get_state
    exit 0 if wiimote.buttons == WiiMote::BTN_HOME
    if wiimote.buttons == WiiMote::BTN_B
      $pos = [ X0, H0, Z0 ]
      $v = [ 0.0, SERVE_SPEED, 0.0 ]
    end
    acc = wiimote.acc.collect { |x| ( x - 120.0 ) / 2.5 }
    if acc[2].abs >= ACC_RISING and Time.new.to_f >= $delay
      $sign = acc[2] > 0 ? +1 : -1 unless $sign
      $strength = [ acc[2].abs, $strength ].max
    elsif $sign
      if acc[2] * $sign <= ACC_FALLING
        if $pos[1] >= MIN_HEIGHT
          # a = Math::PI + 2 * Math.atan2( $v[0], $v[2] ) - Math.atan( ( $pos[2] - NORM_Z ) / L )
          $sounds.push( ( $racket * [ $strength * 0.3 / 50, 0.3 ].min ).to_sint )
          vz = V_MIN + ( V_MAX - V_MIN ) * $strength / 50
          # vz = 12.5
          t = ( SIZE_Z + $pos[2] + DIST_Z / REFLECTION ) / vz
          vy = 0.5 * GRAVITY * t - $pos[1] / t
          vx = -2 * $pos[0] / t
          # vx = Math.tan( a ) * vz
          $v = [ vx, vy, -vz ]
        end
        $sign = nil
        $strength = 0.0
        $delay = Time.new.to_f + MIN_DELAY
      end
    end
  end
  avail = $alsa.avail
  $alsa.write( $sounds.inject( MultiArray.sint( 2, avail ).fill!( 0 ) ) do |x,s|
    n = [ x.shape[1], s.shape[1] ].min
    x[ 0 ... 2, 0 ... n ] + s[ 0 ... 2, 0 ... n ]
  end )
  $sounds = $sounds.select { |s| s.shape[1] > avail }.collect do |s|
    s[ 0 ... 2, avail ... s.shape[1] ]
  end
  GLUT.PostRedisplay
end
GLUT.Init
GLUT.InitDisplayMode GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH
GLUT.InitWindowSize WIDTH, HEIGHT
GLUT.CreateWindow 'Wii Remote'
init
GLUT.DisplayFunc display
GLUT.ReshapeFunc reshape
GLUT.KeyboardFunc keyboard
GLUT.IdleFunc animate
GLUT.MainLoop

See also:

Time Tracking

I recently read an interesting article about time tracking by Sebastian Marshall. There are also two videos about time tracking by the same author. To quote from the website:

To put it simply – I now realize it’s impossible to understand how your life is going without some careful observation. There’s a lot of time each day, and knowing where that time goes, what you ate, what you did and didn’t do… it’s almost impossible to get a good picture of your life without some kind of measuring.

I realised that one could develop a small time tracker in Ruby quite easily. The tracker shown below will ask you questions after showing you the answers of the previous 7 days. In the morning it asks questions of your plans and in the evening it asks you questions in review. You can also run the script multiple times and skip through the questions where you don’t want to modify the answer. I have tried to keep it simple. Quoting from another article by Sebastian Marshall:

Second, remember to start simple. This is to build up momentum and make a workable system you actually use. Do it every day. If you miss a day or two or three, fill in from memory as best as you can. If you fell off a cliff for a while, just reboot. Don’t beat yourself up too much – it solves nothing. We all fall off a cliff sometimes. Also, remember the gains made from living more purposefully are forever – the time you’ve spent well will remains well-spent even if you fall off for a while sometimes. Most people don’t even try, which is why most people don’t succeed.

#!/usr/bin/env ruby
require 'readline'
require 'fileutils'
require 'rexml/document'
include REXML
def track( log, previous, tag, query )
  previous.each_with_index do |prev,i|
    if prev
      if prev.elements[ tag ]
        puts "#{previous.size-i} day(s) ago: #{prev.elements[ tag ].text}"
      end
    end
  end
  item = log.elements[ tag ]
  item = log.add_element tag unless item
  if item.text
    line = "-> #{query} (#{item.text})? "
  else
    line = "-> #{query}? "
  end
  # print line; STDOUT.flush
  # text = STDIN.readline.gsub /[ \r\n]*$/, ''
  text = Readline.readline line
  item.text = text unless text.empty?
end
if File.exist? 'track.xml'
  doc = File.open( 'track.xml', 'r' ) { |f| Document.new f }
else
  doc = Document.new
  doc.add_element 'track'
end
time = Time.new
t = "%4d/%2d/%2d" % [ time.year, time.month, time.day ]
log = doc.root.elements[ "log[@date='#{t}']" ]
previous = ( 1 .. 7 ).collect do |i|
  timep = time - 86400 * i
  tp = "%4d/%2d/%2d" % [ timep.year, timep.month, timep.day ]
  doc.root.elements[ "log[@date='#{tp}']" ]
end.reverse
unless log
  log = Element.new 'log'
  log.add_attribute 'date', t
  doc.root.add_element log
end
list = []
list +=
  [ [ 'wakeup'     , 'At what time did you get up' ],
    [ 'sleep'      , 'How long did you sleep' ],
    [ 'wellness'   , 'Do you feel well' ],
    [ 'objective'  , 'What\'s the objective for today' ] ]
if time.hour >= 14
  list +=
    [ [ 'description', 'What did you do today' ],
      [ 'food', 'What did you eat' ],
      [ 'positive', 'What did I do right to move me towards my goals' ],
      [ 'toimprove', 'What would I do differently if I had the day to live over' ] ]
end
list.each { |tag,query| track log, previous, tag, query }
File.open 'track.xml.part', 'w' do |f|
  doc.write f
end
FileUtils.mv 'track.xml.part', 'track.xml'

I will try to play with this for some time and see whether it has any benefits.

See also:

Wii Remote demonstration

I decided to purchase a Wii Remote to do some small gaming projects in my spare time. The Wii has been around for four years now and it usually comes with the Wii Sports game (I especially enjoyed playing the bowling game with a group of 4 players). Using a PC running GNU/Linux one can communicate with the Wii Remote using the software packages wmgui and wminput. I’ve made a small demonstration video.

The Wii Remote has a 3-axis accelerometer, an IR camera with a microcontroller producing the coordinates of the 10 brightest dots, and several buttons (arrows, A, B, -, Home, +, 1, and 2). Furthermore there are 4 LEDs and a rumble motor. The device uses the Bluetooth standard for wireless communication.

Note that more recently Nintendo started shipping the Wii MotionPlus which adds two gyroscopes for measuring the rotational speed along two axes.

Update: I released the first version of my Ruby bindings for the CWiid library.

Update: I released a Ruby implementation of Johnny Chung Lee’s WiiMote Whiteboard

See also:

Battle Isle

Battle Isle 93 You can download Battle Isle 1 (Moon of Chromos) free of charge at Abandonia and you can run it under GNU/Linux using DOSBox (update: DOSBox also runs on Windows and MacOS). Battle Isle is a turn-based strategy game. The game can be played against the computer as well as against another human player. It always runs in split-screen mode. I.e. each player specifies the moves using half of the screen.

Here’s how to run the game (under GNU/Linux) after downloading it

unzip 'Battle Isle 93 - The Moon of Chromos.zip'
cd BI193
dosbox MOON.EXE -exit
cd ..
  • First player keys
    • '⇦', '⇨', '⇩', and '⇧' to move the cursor
    • '⏎' and cursor movement to select action
  • Second player keys
    • 'x', 'v', 'c', and 'd' to move the cursor
    • 'ctrl' and cursor movement to select action

Here’s a gameplay video of the Amiga version of the game.

See also:

Kubuntu 10.04

I finally decided to update my laptop to Kubuntu 10.04 (a distribution of GNU/Linux based on Debian). As usual I used the Live DVD to just rename the /home folder to /old, delete everything else, and mount the partitions in the advanced setup without formatting them (although like this I wasn’t able to make use of the new EXT4 file system). For new users there is an article how to do a graphical installation of Kubuntu.

KDE4 really has some nice eye candy. Also there are themes for many things and you can just download them using the configuration dialogs. Here’s a screenshot of the Kubuntu Spotlight theme I chose for the login manager. Kubuntu Spotlight KDM

For the boot splash animation I used the Spectrum theme. Spectrum Splash Screen

And here is a screenshot of the desktop in action. Kubuntu 10.04 Lucid Lynx (LTS)

The distribution can be downloaded free of charge at http://www.kubuntu.org/. There are few issues with the HP Compaq nx7400 notebook I am using. So far I’ve found the following problems:

  • NetworkManager disables itself before going into standby and doesn’t come back when disconnecting power during standby mode (one needs to edit the file /var/lib/NetworkManager/NetworkManager.state). Also when using another tool such as pppoeconfig, NetworkManager will disable itself using another configuration file (one needs to edit the file /etc/NetworkManager/nm-system-settings.conf).
  • VoIP settings: One needs to switch everything in Twinkle to ALSA/Default.
  • When not terminated properly, Amarok gives the error message “Amarok is already running!” even though it is not running.
  • Sometimes one needs to restart Amarok because it stops playing after every song.
  • The Shoutcast list is not included in Amarok any more but there is an Amarok script for adding Shoutcast to Amarok.
  • I had to install msn-pecan because Pidgin’s default implementation for MSN has issues.
  • Pidgin won’t connect to ICQ unless Use clientLogin is checked in the advanced account settings.
  • The waste bin has become a plasma widget which is not on the desktop by default.
  • As usual one needs to fetch additional codecs and libdvdcss from the Medibuntu repository in order to be able to play videos using patent-encumbered codecs and DVDs.
  • The file associations for PDF documents are configured to prefer the proprietary Acrobat Reader over the Okular free software.
  • WPA2 wireless keeps disconnecting (iwl3945 driver, deauthenticating by local choice (reason=3)). Installing install the linux backports modules doesn’t seem to help either at this point in time.
  • The default output device for mplayer is xmga. You need to change it to xv.
  • One of the ALSA volume controls for the microphone seems to be mislabelled as the ‘Digital’ input.

Update:

I posted about listing manually selected packages with aptitude on the Ubuntu Forum. This is very useful for generating the command line for installing the same set of packages on a new machine (e.g. my new HP G56-108SA ;)). One can list the manually selected packages as follows:

aptitude search '!~M ~i' -F '%p'

I.e. you can run the following command on one machine:

echo aptitude install `aptitude search '!~M ~i' -F '%p'` > install.sh

Then you copy the resulting file install.sh to the target machine and run

sh ./install.sh

Basically !~M selects not automatically selected packages and ~i selects installed packages. See content of package aptitude-doc-en for more information.

Note that you might have to manually modify the file install.sh if the package names on the target system are different (e.g. 32-bit vs. 64-bit, other proprietary wireless driver, …).

Update:

Here’s a nice example of a dual-screen desktop running the same Kubuntu version.

See also: