package redpong; import java.awt.geom.*; /* This figures out where the ball is, how it bounces, and what the score is. It doesn't know anything about how to display the screen. It provides methods for someone else to know the ball location, etc. so they can draw the screen. */ class RedPongLogic { /* Here we keep track of where the ball is, where the paddles are, and what the score is. Internally, the upper left corner of the court is at 0,0 and the lower right is at courtWidth, courtHeight. The computer's paddle moves up and down the left side of the square. The player's paddle moves up and down the right side of the square. */ private int computerLosses; private int playerLosses; private double courtWidthPixels; private double courtHeightPixels; private double ballRadiusPixels; private double ballLocPixelsX; private double ballLocPixelsY; private double startSpeed; private double startX; private double startY; private double ballSpeed; // pixels per millisecond private double ballDegrees; // 0 degrees is down. private double computerPaddleTop; private double playerPaddleTop; private double paddleHeight; private double paddleWidth; private double pixelsPerKeyClick; // how far to move paddle each keyclick private double waitTime; // walls & paddles: private Rectangle2D.Double [] block; public RedPongLogic(double courtWidth, // court width in pixels double courtHeight, // court height in pixels double ballRadius,//ball rad. as fraction of court height double initBallX, /* initial ball X location as fraction of court width */ double initBallY, /* initial ball Y location as fraction of court height */ double speed) // how many seconds to cross the court { courtWidthPixels = courtWidth; courtHeightPixels = courtHeight; ballRadiusPixels = ballRadius * courtHeight; // Convert seconds per court to pixels per millisecond: startSpeed = courtWidth/(speed*1000.); startX = initBallX * courtWidth; startY = initBallY * courtHeight; resetBall(); pixelsPerKeyClick = courtHeightPixels/20.; // seems like a nice amount paddleHeight = .3 * courtHeightPixels; paddleWidth = .05 * courtWidthPixels; computerPaddleTop = .5 * courtHeightPixels - paddleHeight/2.; playerPaddleTop = .5 * courtHeightPixels - paddleHeight/2.; computerLosses = 0; playerLosses = 0; block = new Rectangle2D.Double[6]; initBlocks(); } private void resetBall() { ballLocPixelsX = startX; ballLocPixelsY = startY; ballDegrees = 90 + RandomRange(20.); ballSpeed = startSpeed; waitTime = 2000; } public double getBallLocPixelsX() { return ballLocPixelsX; } public double getBallLocPixelsY() { return ballLocPixelsY; } public double getBallRadiusPixels() { return ballRadiusPixels; } public double getComputerPaddleTop() { return computerPaddleTop; } public double getPlayerPaddleTop() { return playerPaddleTop; } public double getPaddleWidth() { return paddleWidth; } public double getPaddleHeight() { return paddleHeight; } public int getComputerScore() { return playerLosses; } public int getPlayerScore() { return computerLosses; } public void movePlayer(int direction) { boolean isPos = (direction > 0); // 1 = down, -1 = up double desiredMove = direction * pixelsPerKeyClick; double move = constrainMove(playerPaddleTop, isPos, desiredMove); playerPaddleTop += move; } public Rectangle2D [] getBlocks() { return block; } public void tickUpdate(int msSinceLastUpdate) { if (waitTime > 0.) { waitTime -= msSinceLastUpdate; return; } double msRemaining = msSinceLastUpdate; // Figure out where ball is n milliseconds from now, counting bounces: while (msRemaining > 0.) { msRemaining -= travelOneSegment(msRemaining); } moveComputerPaddle(msSinceLastUpdate); } private double autoMove(double paddleTop, double ms, boolean isBallComing) { double actualCenter = paddleTop+paddleHeight/2.; // e.g. 200 double desiredCenter; if (isBallComing) { desiredCenter = ballLocPixelsY; // e.g. 250 } else { desiredCenter = courtHeightPixels / 2.; } double desiredMove = desiredCenter-actualCenter; boolean isPos = (desiredMove > 0.); double absDesiredMove = isPos? desiredMove : -desiredMove; // how much can we move in the this many milliseconds? // across the court in 1000 milliseconds double paddleSpeed = courtHeightPixels/1000.; double maxMove = paddleSpeed * ms; double absMove = absDesiredMove courtHeightPixels) { constrainedMove = courtHeightPixels - paddleBottom; } } else { if (paddleTop + move < 0.) { constrainedMove = -paddleTop; } } return constrainedMove; } private void moveComputerPaddle(double timeSpanMS) { boolean isBallApproaching = ballDegrees > 180.; double move=autoMove(computerPaddleTop, timeSpanMS, isBallApproaching); computerPaddleTop += move; // call some code to see if we hit the ball on the upswing/downswing // (todo. -wph) } private Rectangle2D.Double initBlock(double x,double y, double w,double h) { // Ideally we'd detect when the ball hits the corner of a block // and treat it specially... Maybe in version 2. // For now we just pad all the blocks by the ball radius. // If the center of the ball hits this padding, the ball is deemed // to have hit the block. return new Rectangle2D.Double(x-ballRadiusPixels, y-ballRadiusPixels, w+2.*ballRadiusPixels, h+2.*ballRadiusPixels); } private Rectangle2D.Double updateY(Rectangle2D.Double r, double y) { // Sets new y into rectangle r, using padding as in initBlock. double x = r.getX(); double w = r.getWidth(); double h = r.getHeight(); r.setRect(x,y-ballRadiusPixels, w,h); return r; } private void initBlocks() { // paddle Y values will change, but everything else is fixed block[0] = initBlock(0, 0, paddleWidth, paddleHeight); // computer paddle block[1] = initBlock(courtWidthPixels-paddleWidth, 0, paddleWidth, paddleHeight); // player paddle // Left and right walls are half a court behind paddles. // To make the math simpler: // Walls extend a court length beyond the boundaries. // Walls have a thickness, 3 pixels. block[2] = initBlock(-courtWidthPixels, -3., 3.*courtWidthPixels, 3.); // top wall block[3] = initBlock(-courtWidthPixels, courtHeightPixels, 3.*courtWidthPixels, 3); // bottom wall block[4] = initBlock(-.5*courtWidthPixels, -courtHeightPixels, 3., 3.*courtHeightPixels); // left wall block[5] = initBlock(1.5*courtWidthPixels, -courtHeightPixels, 3., 3.*courtHeightPixels); // right wall } private double timeToHitVertLine(double x, double topY, double bottomY, double speedX, double speedY) { // System.out.println("Vert: x="+x+" topY="+topY+" bottomY="+bottomY); if (speedX < 0 && ballLocPixelsX < x) return 1000000.; // i.e. never if (speedX > 0 && ballLocPixelsX > x) return 1000000.; // i.e. never double timeToCrossX = (x-ballLocPixelsX)/speedX; double intersectY = timeToCrossX*speedY + ballLocPixelsY; // System.out.println("IntersectY="+intersectY); if (intersectY < topY || intersectY > bottomY) return 1000000.; return timeToCrossX; } private double timeToHitHorzLine(double leftX, double rightX, double y, double speedX, double speedY) { if (speedY < 0 && ballLocPixelsY < y) return 1000000.; // i.e. never if (speedY > 0 && ballLocPixelsY > y) return 1000000.; // i.e. never double timeToCrossY = (y-ballLocPixelsY)/speedY; double intersectX = timeToCrossY*speedX + ballLocPixelsX; if (intersectX < leftX || intersectX > rightX) return 1000000.; return timeToCrossY; } private boolean justHitVert; // yeah, this is a cheezy way to pass this. private double justHitX; private double justHitY; private double timeToCollision(Rectangle2D.Double block, double speedX, double speedY) { // Using current ball location & radius, compute time to collision // with this particular block. double leftX = block.getX(); double rightX = leftX+block.getWidth(); double topY = block.getY(); double bottomY = topY+block.getHeight(); double topTime = timeToHitHorzLine(leftX,rightX,topY, speedX,speedY); double botTime = timeToHitHorzLine(leftX,rightX,bottomY,speedX,speedY); double leftTime = timeToHitVertLine(leftX,topY,bottomY, speedX,speedY); double rightTime= timeToHitVertLine(rightX,topY,bottomY,speedX,speedY); double soonest = 1000000.; // justHitVert and justHitX/Y report what and where we hit. if (topTime < soonest) { soonest = topTime; justHitVert=false; justHitY = topY; } if (botTime < soonest) { soonest = botTime; justHitVert=false; justHitY = bottomY; } if (leftTime < soonest) { soonest = leftTime; justHitVert=true; justHitX = leftX; } if (rightTime < soonest) { soonest = rightTime; justHitVert=true; justHitX = rightX; } return soonest; } private double travelOneSegment(double msRemaining) { /* Figure out how long until we hit an object. If it's longer than the allotted time, great. Just move. If we will hit the object, bounce or die. */ // SpeedX, speedY are in pixels per millisecond. double speedX = Math.sin(Math.toRadians(ballDegrees)) * ballSpeed; double speedY = Math.cos(Math.toRadians(ballDegrees)) * ballSpeed; double segmentTime = msRemaining; block[0] = updateY(block[0], computerPaddleTop); block[1] = updateY(block[1], playerPaddleTop); int thisBlock; double minTime = 10000000.; boolean hitVert = true; double collisionX = 0.; double collisionY = 0.; int collisionBlock = -1; // -1 = collision not known yet for (thisBlock=0; thisBlock<6; thisBlock++) { double thisTime = timeToCollision(block[thisBlock], speedX,speedY); if (thisTime < minTime) { minTime = thisTime; collisionBlock = thisBlock; hitVert = justHitVert; // what kind of edge did we hit? collisionX = justHitX; collisionY = justHitY; } } if (minTime > msRemaining) { // no collision yet ballLocPixelsX += msRemaining * speedX; ballLocPixelsY += msRemaining * speedY; return msRemaining; } ballLocPixelsX += segmentTime * speedX; ballLocPixelsY += segmentTime * speedY; if (hitVert) { ballLocPixelsX = collisionX; if (speedX>0.) { ballLocPixelsX -= .00000001; // just get it off the line } else { ballLocPixelsX += .00000001; } ballDegrees *= -1.; } else { ballLocPixelsY = collisionY; if (speedY>0.) { ballLocPixelsY -= .00000001; } else { ballLocPixelsY += .00000001; } ballDegrees = -ballDegrees + 180.; } // A little random action: ballDegrees += RandomRange(5.); // +/- up to 5 degrees ballDegrees = (ballDegrees % 360 + 360) % 360; // Stay away from vertical motion. That's no fun. if (ballDegrees > 175 && ballDegrees <=180) { ballDegrees=175; } else if (ballDegrees > 180 && ballDegrees < 185) { ballDegrees=185; } // Make it a little bit faster each time... but not TOO fast: ballSpeed *= 1.02; if (ballSpeed > courtWidthPixels*5/1000) { ballSpeed = courtWidthPixels*5/1000; // max 200ms per screen } if (collisionBlock == 4) { computerLosses++; } else if (collisionBlock == 5) { playerLosses++; } if (collisionBlock==4 || collisionBlock==5) { resetBall(); } return minTime; } private double RandomRange(double maxOffset) { return maxOffset * (Math.random()*2.-1.); } }