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