Pingpong

Arjen Markus Not quite perfect, but I intend it as an example for my Young Programmers' Project [1 ] and I did not want to cloud the code with lots of details.

This is a colourful resurrection of the first graphical game for home enertainment that I have seen. Use the cursor keys to move the green rectangle (the "keeper" in the code). No way to influence the speed - but that is going to be an exercise in that chapter...

# pingpong.tcl --
#    Play the "ancient" game of PingPong
#
package require Tk

# createField --
#    Create the playing field and the score board
# Arguments:
#    None
# Result:
#    None
# Side effects:
#    Filling the state array and creating the canvas to play on
#
proc createField { } {
    global ppdata

    set ppdata(width)  300
    set ppdata(height) 200
    set ppdata(keeper_height) 20

    canvas .c -width $ppdata(width) -height $ppdata(height) \
              -background white
    pack   .c -fill both

    #
    # Note: the vertical coordinate increases from top to bottom
    #
    set htop    [expr {($ppdata(height)-$ppdata(keeper_height))/2}]
    set hbottom [expr {($ppdata(height)+$ppdata(keeper_height))/2}]

    set ppdata(keeper_ymin) \
        [expr {$ppdata(keeper_height)/2}]
    set ppdata(keeper_ymax) \
        [expr {$ppdata(height)-$ppdata(keeper_height)/2}]
    set ppdata(keeper_ystep) \
        [expr {$ppdata(keeper_height)/3}]

    set wleft     5
    set wright   15

    set ppdata(keeper_right) $wright
    set ppdata(keeper_y)     [expr {$ppdata(height)/2}]

    set ppdata(keeper) \
       [.c create rectangle $wleft $htop $wright $hbottom \
           -outline black -fill forestgreen]

    set wleft    [expr {$ppdata(width)-20}]
    set wright   [expr {$ppdata(width)-10}]
    set htop     [expr {$ppdata(height)/2-5}]
    set hbottom  [expr {$ppdata(height)/2+5}]

    set ppdata(ball_x) $wleft
    set ppdata(ball_y) $ppdata(keeper_y)
    set ppdata(ball_xinit) $wleft
    set ppdata(ball_yinit) $ppdata(keeper_y)

    #
    # Initialise the ball
    #
    set ppdata(ball) \
       [.c create oval $wleft $htop $wright $hbottom \
           -outline black -fill yellow]
    set ppdata(ball_speed) 5.0
    newBall

    set wleft    [expr {$ppdata(width)-$ppdata(keeper_height)-5}]
    set wright   [expr {$ppdata(width)-5}]
    set htop     [expr {($ppdata(height)-$ppdata(keeper_height))/2}]
    set hbottom  [expr {($ppdata(height)+$ppdata(keeper_height))/2}]
    set ppdata(shooter) \
       [.c create oval $wleft $htop $wright $hbottom \
           -outline black -fill purple]

    frame  .frm
    label  .frm.keeper  -textvariable ppdata(keeper_score)  \
       -font "helvetica 20"
    label  .frm.shooter -textvariable ppdata(shooter_score) \
       -font "helvetica 20"
    label  .frm.inbetween -text "             " \
       -font "helvetica 20"

    button .frm.reset -text "Reset" -command resetScore -width 10
    button .frm.exit  -text "Exit"  -command exit       -width 10

    set ppdata(keeper_score)  0
    set ppdata(shooter_score) 0
    pack  .frm -fill x -side bottom
    grid  .frm.keeper .frm.inbetween .frm.shooter
    grid  .frm.reset  x              .frm.exit

    bind  .c <Key-Up>   {moveKeeper up}
    bind  .c <Key-Down> {moveKeeper down}

    #
    # Let the canvas have the input focus, otherwise the keeper
    # can not be moved
    #
    focus .c
    wm focus .
}

# moveKeeper --
#    Move the keeper up or down
# Arguments:
#    dir      Direction in which to move the rectangle
# Result:
#    None
# Side effects:
#    The rectangle is moved up or down (if possible)
#
proc moveKeeper { dir } {
    global ppdata

    if { $dir == "up" } {
       if { $ppdata(keeper_y) > $ppdata(keeper_ymin) } {
          incr ppdata(keeper_y) -$ppdata(keeper_ystep)
          .c move $ppdata(keeper) 0 -$ppdata(keeper_ystep)
       }
    }

    if { $dir == "down" } {
       if { $ppdata(keeper_y) < $ppdata(keeper_ymax) } {
          incr ppdata(keeper_y)  $ppdata(keeper_ystep)
          .c move $ppdata(keeper) 0 $ppdata(keeper_ystep)
       }
    }
}

# moveBall --
#    Move the ball to the left (note: it bounces off the wall and the
#    keeper)
# Arguments:
#    None
# Result:
#    None
# Side effects:
#    The ball is moved, possibly either score is increased
#
proc moveBall { } {
    global ppdata

    if { $ppdata(ball_x) > 0 } {
       .c move $ppdata(ball) $ppdata(ball_xstep) $ppdata(ball_ystep)
       foreach {xmin ymin xmax ymax} [.c coords $ppdata(ball)] {break}
       set ppdata(ball_x) [expr {($xmin+$xmax)/2.0}]
       set ppdata(ball_y) [expr {($ymin+$ymax)/2.0}]

       #set ppdata(ball_x) [expr {$ppdata(ball_x)+$ppdata(ball_xstep)}]
       #set ppdata(ball_y) [expr {$ppdata(ball_y)+$ppdata(ball_ystep)}]
    } else {
       #
       # The keeper has missed, "new" ball
       #
       incr ppdata(shooter_score)
       newBall
    }
    #
    # Reflection off the top wall
    #
    if { $ppdata(ball_y) < 0 } {
       set ppdata(ball_y)     [expr {-$ppdata(ball_y)}]
       set ppdata(ball_ystep) [expr {-$ppdata(ball_ystep)}]
       .c move $ppdata(ball) 0 $ppdata(ball_ystep)
    }
    #
    # Reflection off the bottom wall
    #
    if { $ppdata(ball_y) > $ppdata(height) } {
       set dy                 [expr {$ppdata(height)-$ppdata(ball_y)}]
       set ppdata(ball_y)     [expr {2.0*$ppdata(height)-$ppdata(ball_y)}]
       set ppdata(ball_ystep) [expr {-$ppdata(ball_ystep)}]
       .c move $ppdata(ball) 0 $dy
    }
    #
    # Reflection off the keeper:
    # - let the ball go on for another two seconds, then move it to the
    #   initial position
    #
    if { $ppdata(ball_x) < $ppdata(keeper_right)+2 } {
       if { abs($ppdata(keeper_y)-$ppdata(ball_y)) < 10 } {
          set ppdata(ball_x)     \
             [expr {2*$ppdata(keeper_right)-$ppdata(ball_x)}]
          set ppdata(ball_xstep) [expr {-$ppdata(ball_xstep)}]

          .c move $ppdata(ball) $ppdata(ball_xstep) 0

          incr ppdata(keeper_score)
          after 2000 newBall
       }
    }

    after 50 moveBall
}

# newBall --
#    Shoot a new ball
# Arguments:
#    None
# Result:
#    None
# Side effects:
#    The ball is moved back to the initial position, it is given a new
#    direction
#
proc newBall { } {
    global ppdata

    set dx [expr {$ppdata(ball_xinit)-$ppdata(ball_x)}]
    set dy [expr {$ppdata(ball_yinit)-$ppdata(ball_y)}]
    .c move $ppdata(ball) $dx $dy

    set angle [expr {3.1415926*(1.0+(0.5-rand())/2.0)}]

    set ppdata(ball_x) [expr {$ppdata(ball_x)+$dx}]
    set ppdata(ball_y) [expr {$ppdata(ball_y)+$dy}]

    set ppdata(ball_xstep) [expr {$ppdata(ball_speed)*cos($angle)}]
    set ppdata(ball_ystep) [expr {$ppdata(ball_speed)*sin($angle)}]
}

# resetScore --
#    Reset the score (simply set the two variables to zero)
#    keeper)
# Arguments:
#    None
# Result:
#    None
# Side effects:
#    The score variables are set to zero
#
proc resetScore { } {
    global ppdata

    set ppdata(keeper_score)  0
    set ppdata(shooter_score) 0
}

#
# The main loop: set up the field and go
#
createField
after 100 moveBall

xmav000 not quite perfect, but if you are a player and if you want, you might try to "catch" the ball and getting lots of points at once. to do so you hit the ball with the top or the bottom side of the green box. with a bit practise you can keep the ball within the green box for a short time by moving the box in the direction the ball wants to go. I did reach 37 points at once a few times. dont know what limits this. anyone gets higher? :)

AM Theoretically, you can reach 40: after the first collision, the ball remains in the neighbourhood for another 2 seconds and the position (and score) is updated with 50 ms intervals. But I will fix this!!!!

(Well, on my YPP page, that is! :)