## Thursday, February 2, 2012

### A Model of a Ping Pong Game

Last year in the programming languages course at Halmstad University, students worked in groups to develop different parts of a ping pong game.  Later, Yingfu Zeng combined these projects into one model that streamlined these components and further developed below.  The result is what you find below in the rest of this post.

The model below was used in the first tournament in cyber physical systems course.  Out of seven entries, the winning entry was team Virtue by Victor Vasilev and Carlos Fuentes.  The winning entry was able to score 7.5 out of a maximum of 12 possible points.   The benchmark model, team WiffWaff by Adam Duracz and Yingfu Zeng, shows that it is possible to score 11 out of 12 points.

NEW!  Check out the videos and analysis!

/**
* Program:   3-D ping pong
* Author :   Yingfu Zeng, Walid Taha
* Date   :   2012/02/11
* License:   BSD, GPL(V2), or other by agreement with Walid Taha
**/
class Ball ()
private
mode = "Fly";
k_z  = [1,1,-0.99];       // Coefficient of restitution
k2   = 1/6;               // Coefficient of the air resistance
p    = [0,0,0.5];         // Position of the ball
p'   = [5,1,-3];
p''  = [0,0,0];
_3D  = ["Sphere",[0,0,0.5],0.03,[1,1,1],[0,0,0]];
end
_3D [=] ["Sphere",p,0.03,[1,1,1],[0,0,0]];
// Valid modes
if mode ~= "Fly" && mode ~= "Bounce" && mode ~= "Freeze"
mode = "Panic!";
end;
switch mode
case "Fly"
if dot(p,[0,0,1]) < 0 && dot(p',[0,0,1])< 0
mode = "Bounce";
else
p'' [=] -k2 * norm(p') * p' + [0,0,-9.8];
end;
case "Bounce"
p'   =  p' .* k_z;    // Bouncing  will lose some energy
mode = "Fly";
case "Freeze"           // The ball becomes red to show what is going wrong
p'  [=] [0,0,0]; p'' [=] [0,0,0];
_3D [=] ["Sphere",p,0.03,[1,0,0],[0,0,0]];
case "Panic!"
end
end

class BatActuator(p1)
private
p       = p1;
p'      = [0,0,0];
angle   = [0,0,0];
energy  = 0;
energy' = 0;
end
if norm(p') > 5
p' = p'/norm(p') * 5 ;
end;
energy' [=] norm(p');
end

class Bat(n,p1)
private
p     = p1;
p'    = [0,0,0];
angle = [0,0,0.1];
displayAngle = [0,0,0];
mode  = "Run";
_3D   = ["Cylinder",p1,[0.15,0.05],[0.1,0.1,0.1],[0,0,0.5]];
end
switch mode
case "Run"
if n == 2
displayAngle  [=] [0,dot(angle,[0,0,1])*(3.14/2)/norm(angle),
dot(angle,[0,1,0])*(3.14/2)/norm(angle)]+[0,0,3.14/2];
_3D            [=] ["Cylinder",p+[0.05,0,0],[0.15,0.05],
[0.1,0.1,0.1],displayAngle];
else
displayAngle [=] [dot(angle,[0,0,1])*(3.14/2),0,
dot(angle,[0,1,0])*(3.14/2)]+[0,0,3.14/2];
_3D           [=] ["Cylinder",p+[-0.05,0,0],[0.15,0.05],
[1,0.1,0.1],-1 * displayAngle];
end;
case "Rest"
p'            [=] [0,0,0];
_3D           [=] ["Box",p+[-0.05,0,0],[0.3,0.3,0.3],
[1,1,0.1],-1 * displayAngle];
end
end

/**
*Position and velocity of ball(ballp,ballv) always provided estimately;
*Once player decides to hit the ball, change the hit variable to true,
*the Game class will notice and caculate the output velocity of the ball.
**/
class Player(n)
private
mode      = "Wait";
bounced   = false;       // Tell whether the ball bounced or not
serve = false;           // The Game class will set serve flag to true
hit   = false;           // when it's your turn
count = 0;
ballv = [0,0,0];
ballp = [0,0,0];
batp  = [1.6,0,0.2];
v     = [0,0,0];         // Bat's speed
batAngle   = [0,0,0.1];  // Normal vector of the bat's plane
batAngle'  = [0,0,0];
// Player(1) starts at [-1.6,0,0.2], Player(2) starts at [1.6,0,0.2]
startPoint = [1.6*(-1)^n,0,0.2];
t   = 0;
t'  = 1;
end
if mode ~= "Wait" && mode ~= "Prepare" && mode ~= "Hit"
mode = "Panic!";
end;
t'  [=] 1;
switch mode
case "Wait"               // While waiting, moving the bat to starting point
count      = 0;
if n == 1
v         [=] startPoint-batp;
else
v         [=] startPoint + [0,0.75,0] - batp;
end;
batAngle' [=] [0,0,0]-batAngle;
hit    = false;
if serve == true
mode    = "Prepare";
bounced = false;
else
mode = "Wait";
end;
case "Prepare"             // Prepare to hit the ball
if bounced == true        // After the ball has bounced,
// start moving the bat towards the ball
v [=] (ballp-batp).*[0,20,0] + (ballp-batp).*[0,0,25] +
(ballp+[0.12*(-1)^n,0,0]-batp).*[25,0,0];
if norm(batp - ballp)<0.15 && abs(dot(ballp,[1,0,0])) >=
abs(dot(startPoint,[1,0,0]))
count = count+1;
mode  = "Hit";
end;
end;
// When the ball has bounced and it is at the highest position
if count > 0 && dot(ballv,[0,0,1]) < 0.1 && bounced == true
mode = "Hit";     // This player decide to hit.
end;
if dot(ballp,[0,0,1]) < 0 && bounced == false
bounced = true;
end;
if(serve ~= true)
mode = "Wait";
end;
case "Hit"           // Decide how you want hit the ball,
if n == 2
if(t<1||t>5)       // you may want to check the formulas
// in the BallActuator() class
v        = [-1.38,0.40,1.2];
batAngle = [0.9471,0.25,-0.2];
else
if t > 4 && t < 5
v        = [-0.88,-0.5,0.2];
batAngle = [0.9471,0.25,-0.2];
else
v        = [-1.7,-0.2,3.86];
batAngle = [0.96,-0.1,-0.2258];
end;
end;
else
if(dot(ballv,[0,1,0]) < 0)
v        = [0.1,-0.15,3.85];
batAngle = [-0.938,-0.162,-0.29];
else
v        = [1,0,2.85];
batAngle = [-0.938,0.202,-0.29];
end;
end;
serve  = false;
hit    = true;
mode   = "Wait";
case "Panic!"
end
end

class Table()   // The table
private
// Table
_3D = [["Box", [0,0,-0.05],[3,1.5,0.03],[0.1,0.1,1.0],[0,0,0]],
// TableBases 1~4
["Box", [-1.4,0.6,-0.3-0.04], [0.05,0.05,0.6], [0.8,0.8,0.8],[0,0,0]],
["Box", [-1.4,-0.6,-0.3-0.04], [0.05,0.05,0.6], [0.8,0.8,0.8],[0,0,0]],
["Box", [1.4,-0.6,-0.3-0.04], [0.05,0.05,0.6], [0.8,0.8,0.8],[0,0,0]],
["Box", [1.4,0.6,-0.3-0.04], [0.05,0.05,0.6], [0.8,0.8,0.8],[0,0,0]],
// Net
["Box", [0,0,0.125-0.02], [0.05,1.5,0.25], [0.2,0.8,0.2],[0,0,0]],
// MiddleLine
["Box", [0,0,0],[3,0.02,0.02-0.02],[1,1,1],[0,0,0]]]
end
end

class BallActuator()  // Calculate result of impact
private
mode="Initialize";
v1 = [0,0,0];      // Input ball speed
v2 = [0,0,0];      // Output ball speed
v3 = [0,0,0];      // Bat's speed during the impact
angle = [0,0,0];   // Bat's normal vector
done  = false;
action = 0;
end
if mode ~= "Initialize" && mode ~= "Calculate" && mode ~= "Wait"
mode = "Panic!";
end;
switch mode
case "Initialize"
done[=]false;
if action == 1
mode = "Calculate";
end;
case "Calculate"
v2     = v1-dot(2.*(v1-v3),angle)*angle;
action = 0;
if action == 0
mode = "Wait";
end;
case "Wait"
done [=] true;
case "Panic!"
end
end

// Sample the velocity of the ball and feed back to the players.
class BallObserver()
private
mode = "Sample";
p  = [0,0,0];
v  = [0,0,0];
pp = [0,0,0];
ap = [0,0,0];
t  = 0;
t' = 1;
end
t'[=]1;
if mode ~= "Sample" && mode ~= "Estimate0" && mode ~= "Estimate1"
mode = "Panic!";
end;
switch mode
case "Sample"
if t > 0
pp  = p;
t   = 0;
mode= "Estimate0"
end;
case "Estimate0"
if t == 0.01   // Calculate the average speed
ap   = p;
mode = "Estimate1";
end;
case "Estimate1"
v    = dot((ap-pp),[1,0,0])/0.01*[1,0,0]+dot((ap-pp),[0,0,1])/0.01*[0,0,1]+
dot((ap-pp),[0,1,0])/0.01*[0,1,0];
mode = "Sample";
t    = 0;
case "Panic!"
end
end

class Referee()  // This class will monitors the whole process of the game.
private
mode="Initialize";
x = 0;x' = 0;
z = 0;z' = 0;
y = 0;
t = 0;t' = 1;
player1Score = 0;
player2Score = 0;
serveNumber  = 2;
lastHit      = 0;
reason       = "Nothing";
checked      = false;    // For the net checking
bounced      = false;
restart      = 0;        // Tell the Game to restart
acknowledged = 0;        // Check if the Game class has received
//  the restart signal
bounceTime   = 0;
status       = "Normal"
end
if mode ~= "Initialize" && mode ~= "Player1Lost" && mode ~= "Player2Lost"
&& mode ~= "SendMessage" && status ~= "Normal" && reason ~= "Nothing"
&& status ~= "Report" && reason ~= "BallOutOfBoundary"
&& reason ~= "BallBouncedTwice" && reason ~= "BallTouchNet"
mode = "Panic!";
end;
t'[=]1;
if z<0.05 && z'<0 && status == "Normal"  // Check if anyone fouls
if (abs(y)>0.78||abs(x)>1.53) && status == "Normal"
reason     = "BallOutOfBoundary";
if bounced == false
if x>0
mode = "Player1Lost";
else
mode = "Player2Lost";
end;
else
if bounced == "YesIn2"    // The ball has bounced in player2's court,
mode = "Player2Lost"     // and out of boundary now, so player2 lose.
end;
if bounced == "YesIn1"
mode = "Player1Lost";
end;
end;
status = "Report";
end;
if(abs(y)<0.78 && abs(x)<1.53) && bounced ~= false
&& t>(bounceTime+0.1) && status=="Normal"
// The ball has bounced twice in player2's court
if bounced == "YesIn2" && x > 0
mode   = "Player2Lost";
reason = "BallBouncedTwice";
bounceTime = t;
end;
// The ball has bounced twice in player1's court
if bounced == "YesIn1" && x < 0
mode   = "Player1Lost";
reason = "BallBouncedTwice";
bounceTime = t;
end;
end;
if x<0 && x>-1.5 && bounced == false && status == "Normal"
bounced    = "YesIn1";
bounceTime = t;
end;
if x>=0 && x<1.5 && bounced == false && status == "Normal"
bounced    = "YesIn2";
bounceTime = t;
end;
end;

if bounced == "YesIn1" && x>0 && status == "Normal"
bounced = false
end;
if bounced == "YesIn2" && x<=0 && status == "Normal"
bounced = false
end;
// Time to check if the ball touches the net
if abs(x)<0.025 && t>0.1 && checked == false && status == "Normal"
if z<0.25
if x'>0
mode   = "Player1Lost";
else
mode   = "Player2Lost"
end;
reason  = "BallTouchNet";
checked = true;
end;
end;
switch mode
case "Initialize"
case "Player1Lost"
player2Score = player2Score+1;
mode = "SendMessage";
case "Player2Lost"
player1Score = player1Score+1;
mode = "SendMessage";
case "SendMessage"
t = 0; // Wait until the Game class gets the restart signal
restart = 1;
if acknowledged == 1
mode = "Initialize";
acknowledged = 0;
restart = 0;
status  = "Normal";
checked = false;
bounced = false;
end;
case "Panic!"
end
end

/**
* The parent of all the other classes, who controls the
* whole process of the game.
**/
class Game ()
private
ball    = create Ball ();
ballob  = create BallObserver();
actuator= create BallActuator();
batActuator1 = create BatActuator([-1.6,0,0.2]);
batActuator2 = create BatActuator([1.6,0,0.2]);
player1 = create Player(1);
player2 = create Player(2);
bat1    = create Bat(1,[-1.6,0,0.2]);
bat2    = create Bat(2,[1.6,0,0.2]);
table   = create Table();
gameMonitor = create Referee();
mode    = "Player2Serve";       // Player2 starts first
player2Score = 0;
player1Score = 0;
serveNumber  = 2;
t  = 0;
t' = 1;
maxEnergy    = 18;
end
if mode ~= "Restart" && mode ~= "Player1Serve" && mode ~= "Player2Serve"
&& mode ~= "Impact"  && mode ~= "Freeze" && mode ~= "ChangeSide"
&& mode ~= "Act"
mode = "Panic!"
end;
t'[=]1;
gameMonitor.x  [=] dot(ball.p,[1,0,0]);
gameMonitor.x' [=] dot(ball.p',[1,0,0]);
gameMonitor.z  [=] dot(ball.p,[0,0,1]);
gameMonitor.z' [=] dot(ball.p',[0,0,1]);
gameMonitor.y  [=] dot(ball.p,[0,1,0]);
gameMonitor.serveNumber [=] serveNumber;
player1Score  [=] gameMonitor.player1Score;
player2Score  [=] gameMonitor.player2Score;
ballob.p          [=] ball.p;
player1.ballp     [=] ballob.p;
player2.ballp     [=] ballob.p;
player1.ballv     [=] ballob.v;
player2.ballv     [=] ballob.v;
if bat1.mode ~= "Rest"
batActuator1.p' [=] player1.v;
end;
if bat2.mode ~= "Rest"
batActuator2.p' [=] player2.v;
end;
player1.batp  [=] bat1.p;
player2.batp  [=] bat2.p;
batActuator1.angle [=] player1.batAngle;
batActuator2.angle [=] player2.batAngle;
bat1.p  [=] batActuator1.p;
bat1.p' [=] batActuator1.p';
bat2.p  [=] batActuator2.p;
bat2.p' [=] batActuator2.p';
bat1.angle [=] batActuator1.angle;
bat2.angle [=] batActuator2.angle;
if batActuator1.energy > maxEnergy
bat1.mode = "Rest";
bat1.p'   = [0,0,0];
batActuator1.p' [=] [0,0,0];
end;
if batActuator2.energy > maxEnergy
bat2.mode = "Rest";
bat2.p'   = [0,0,0];
batActuator2.p' [=] [0,0,0];
end;
switch mode
case "Restart" // Put everything back to the starting point
ball.p            = [0,0,0.5];
ball.p'           = [5,1,-3];
bat2.p            = [1.6,0,0.2];
player2.batp      = [1.6,0,0.2];
player2.v         = [0,0,0];
player2.batAngle  = [0.01,0,0];
player2.bounced   = false;
player2.ballp     = [1.6,0,0.2];
bat1.p            = [-1.6,0,0.2];
player1.batp      = [-1.6,0,0.2];
player1.v         = [0,0,0];
player1.batAngle  = [0.01,0,0];
player1.bounced   = false;
player1.ballp     = [-1.6,0,0.2];
batActuator1.p    = [-1.6,0,0.2];
batActuator2.p    = [1.6,0,0.2];
serveNumber       = 2;
gameMonitor.bounced      = false;
gameMonitor.checked      = false;
gameMonitor.acknowledged = 1;
mode         = "Player2Serve";
player1.mode = "Wait";
player2.mode = "Wait";
case "Player2Serve" // Player 2 is serving
player1.serve [=] false;
player2.serve [=]  true;
if player2.hit == true && norm(bat2.p - ball.p) < 0.15
mode = "Impact"
end;
if gameMonitor.restart == 1
mode = "Freeze";
t    = 0;
end;
case "Player1Serve" // Player 1 is serving
player2.serve [=] false;
player1.serve [=] true;
if player1.hit == true && norm(bat1.p - ball.p) < 0.15
mode = "Impact"
end;
if gameMonitor.restart == 1
mode = "Freeze";
t    = 0;
end;
case "Impact" // When one player hits the ball
actuator.v1 = ball.p';
if serveNumber == 2 // Give player2's data to actuator
batActuator2.p' = player2.v;
bat2.p'         = batActuator2.p';
actuator.v3     = bat2.p';
bat2.angle      = player2.batAngle;
actuator.angle  = bat2.angle;
actuator.action = 1; // Tell actuator to act
gameMonitor.lastHit = 2;
mode = "Act";
if gameMonitor.restart == 1
mode = "Freeze";
t = 0;
end;
end;
if serveNumber == 1 // Give player1's data to actuator
batActuator1.p' = player1.v;
bat1.p'         = batActuator1.p';
actuator.v3     = bat1.p';
bat1.angle      = player1.batAngle;
actuator.angle  = bat1.angle;
actuator.action = 1; // Tell actuator to act
gameMonitor.lastHit = 1;
mode = "Act";
if gameMonitor.restart == 1
mode = "Freeze";
t    = 0;
end;
end
case "Act" // Wait till actuator finish
if gameMonitor.restart == 1
mode = "Freeze";
t    = 0;
end;
if actuator.done == true
ball.p'       = actuator.v2;
actuator.mode = "Initialize";
mode          = "ChangeSide";
end;
case "ChangeSide" // Change the serve number
if gameMonitor.restart == 1
mode = "Freeze";
t    = 0;
end;
if serveNumber == 2 && dot(ball.p,[1,0,0]) >0 && gameMonitor.restart ~= 1
serveNumber     = 1;
mode            = "Player1Serve";
player1.mode    = "Wait";
player1.bounced = false;
end;
if serveNumber == 1 && dot(ball.p,[1,0,0]) <= 0 && gameMonitor.restart ~= 1
serveNumber     = 2;
mode            = "Player2Serve";
player2.mode    = "Wait";
player2.bounced = false;
end;
// When someone fouls, showing what's going wrong for 1 second
case "Freeze"
if t<1
ball.mode = "Freeze";
else
mode      = "Restart";
ball.mode = "Fly";
end;
case "Panic!"
end
end

class Main(simulator)
private
mode = "Initialize";
end
switch mode
case "Initialize"
simulator.endTime = 20;
create Game();
mode = "Persist";
case   "Persist"
end
end