001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.demos.sandbox;
031
032import java.awt.event.KeyEvent;
033import java.awt.event.KeyListener;
034import java.text.AttributedCharacterIterator.Attribute;
035import java.text.AttributedString;
036import java.util.HashMap;
037import java.util.Random;
038
039import javax.swing.JFrame;
040import javax.swing.SwingUtilities;
041
042import org.openimaj.image.MBFImage;
043import org.openimaj.image.colour.ColourSpace;
044import org.openimaj.image.colour.RGBColour;
045import org.openimaj.image.renderer.MBFImageRenderer;
046import org.openimaj.image.typography.FontStyle;
047import org.openimaj.image.typography.hershey.HersheyFont;
048import org.openimaj.math.geometry.line.Line2d;
049import org.openimaj.math.geometry.point.Point2dImpl;
050import org.openimaj.math.geometry.shape.Circle;
051import org.openimaj.math.geometry.shape.Rectangle;
052import org.openimaj.math.geometry.transforms.TransformUtilities;
053import org.openimaj.video.Video;
054import org.openimaj.video.VideoDisplay;
055
056import Jama.Matrix;
057
058/**
059 * A rather simple implementation of Pong
060 * 
061 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
062 * 
063 */
064public class Pong extends Video<MBFImage> {
065        private static final float PADDLE_RADIUS = 0.07f;
066        private static final float BALL_SIZE = 0.02f;
067        private static final float PADDLE_VELOCITY = 0.01f;
068
069        private static final float BORDER_TOP = 0.1f;
070        private static final float BORDER_BOTTOM = 0.05f;
071
072        private static final float SPEED_MULTIPLIER = 1.2f;
073        private static final float INITIAL_VELOCITY = 5.0f;
074
075        private final Rectangle borderTop;
076        private final Rectangle borderBottom;
077
078        protected Circle paddleLeft;
079        protected Circle paddleRight;
080        private Point2dImpl paddleLeftPoint;
081        private Point2dImpl paddleRightPoint;
082        private Circle ball;
083        private Point2dImpl ballCentre;
084
085        private final MBFImage lastFrame;
086        private final MBFImage frame;
087        private final MBFImageRenderer renderer;
088
089        private Point2dImpl ballVelocity;
090
091        private int scoreLeft = 0;
092        private int scoreRight = 0;
093        private final int frame_height;
094        private final int frame_width;
095        private final HashMap<Attribute, Object> redText;
096        private final HashMap<Attribute, Object> blueText;
097        private final HashMap<Attribute, Object> allFont;
098
099        public Pong(final int width, final int height) {
100
101                this.redText = new HashMap<Attribute, Object>();
102                this.redText.put(FontStyle.COLOUR, RGBColour.RED);
103
104                this.blueText = new HashMap<Attribute, Object>();
105                this.blueText.put(FontStyle.COLOUR, RGBColour.BLUE);
106
107                this.allFont = new HashMap<Attribute, Object>();
108                this.allFont.put(FontStyle.FONT, HersheyFont.ROMAN_SIMPLEX);
109                this.allFont.put(FontStyle.FONT_SIZE, 40);
110
111                this.frame_height = height;
112                this.frame_width = width;
113                this.lastFrame = new MBFImage(width, height, ColourSpace.RGB);
114                this.frame = new MBFImage(width, height, ColourSpace.RGB);
115                this.renderer = this.frame.createRenderer();
116
117                this.borderTop = new Rectangle(0, 0, width, height * Pong.BORDER_TOP);
118                this.borderBottom = new Rectangle(0, height * (1 - Pong.BORDER_BOTTOM), width, height * Pong.BORDER_BOTTOM);
119
120                this.initMatch();
121
122                this.getNextFrame();
123        }
124
125        private void initMatch() {
126                this.scoreLeft = 0;
127                this.scoreRight = 0;
128
129                this.initGame();
130        }
131
132        private void initGame() {
133                this.lastFrame.fill(RGBColour.BLACK);
134                this.frame.fill(RGBColour.BLACK);
135                final Random r = new Random();
136                this.paddleLeftPoint = new Point2dImpl(0, 0.5f * this.frame_height);
137                this.paddleLeft = new Circle(this.paddleLeftPoint, Pong.PADDLE_RADIUS * this.frame_height);
138                this.paddleRightPoint = new Point2dImpl(this.frame_width, 0.5f * this.frame_height);
139                this.paddleRight = new Circle(this.paddleRightPoint, Pong.PADDLE_RADIUS * this.frame_height);
140
141                this.ballCentre = new Point2dImpl(0.5f * this.frame_width, 0.5f * this.frame_height);
142                this.ball = new Circle(this.ballCentre, Pong.BALL_SIZE * this.frame_width);
143
144                this.ballVelocity = new Point2dImpl();
145
146                // float vx = (float) (Math.random() - 0.5);
147                // float vy = (float) (Math.random() - 0.5);
148                //
149                // if (0.5 * vy > vx) {
150                // float tmp = vx;
151                // vx = vy;
152                // vy = tmp;
153                // }
154                //
155                // float mult = (float) (INITIAL_VELOCITY / Math.sqrt(vx*vx + vy*vy));
156                // vx*=mult;
157                // vy*=mult;
158                float vx = r.nextBoolean() ? 1f : -1f;
159                float vy = r.nextFloat() * 2 + -1f;
160                final float mult = (float) (Pong.INITIAL_VELOCITY / Math.sqrt(vx * vx + vy * vy));
161                vx *= mult;
162                vy *= mult;
163                this.ballVelocity = new Point2dImpl(vx, vy);
164        }
165
166        @Override
167        public MBFImage getNextFrame() {
168                // draw scene
169                this.updateBall();
170
171                this.frame.fill(RGBColour.BLACK);
172                this.renderer.drawShapeFilled(this.borderTop, RGBColour.GRAY);
173                this.renderer.drawShapeFilled(this.borderBottom, RGBColour.GRAY);
174
175                this.renderer.drawShapeFilled(this.paddleLeft, RGBColour.RED);
176                this.renderer.drawShapeFilled(this.paddleRight, RGBColour.BLUE);
177
178                this.renderer.drawShapeFilled(this.ball, RGBColour.WHITE);
179                // if(contactPoint!=null) renderer.drawPoint(contactPoint, 1.0f, 10);
180                final int scorLeftLength = new String("" + this.scoreLeft).length();
181                final int scorRightLength = new String("" + this.scoreRight).length();
182                final int sepLength = new String(" : ").length();
183                final String allString = this.scoreLeft + " : " + this.scoreRight;
184                final AttributedString str = new AttributedString(allString);
185                str.addAttributes(this.allFont, 0, allString.length());
186                str.addAttributes(this.redText, 0, scorLeftLength);
187                str.addAttributes(this.blueText, scorLeftLength + sepLength, scorLeftLength + sepLength + scorRightLength);
188                this.renderer.drawText(str, this.frame_width / 2, 40);
189
190                this.lastFrame.drawImage(this.frame, 0, 0);
191
192                this.currentFrame++;
193
194                return this.frame;
195        }
196
197        private void updateBall() {
198                float newX = this.ballCentre.x + this.ballVelocity.x;
199                float newY = this.ballCentre.y + this.ballVelocity.y;
200
201                if (newX < 0) {
202                        this.initGame();
203                        this.scoreRight++;
204                        return;
205                }
206
207                if (newX > this.frame_width) {
208                        this.initGame();
209                        this.scoreLeft++;
210                        return;
211                }
212
213                if (newY < this.frame_height * (Pong.BORDER_TOP + Pong.BALL_SIZE)) {
214                        newY = 2 * this.frame_height * (Pong.BORDER_TOP + Pong.BALL_SIZE) - newY;
215                        this.ballVelocity.y = -this.ballVelocity.y;
216                }
217
218                if (newY > this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.BALL_SIZE)) {
219                        newY = 2 * this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.BALL_SIZE) - newY;
220                        this.ballVelocity.y = -this.ballVelocity.y;
221                }
222
223                final double dLeftPaddle = Line2d.distance(this.paddleLeftPoint, new Point2dImpl(newX, newY));
224                final double dRightPaddle = Line2d.distance(this.paddleRightPoint, new Point2dImpl(newX, newY));
225
226                if (dLeftPaddle <= this.paddleLeft.getRadius() + this.ball.getRadius()) {
227
228                        // contactPoint = contactPoint(paddleLeft,this.ball);
229                        final Point2dImpl contactDelta = this.normContactDelta(this.paddleLeft, this.ball);
230
231                        newX = this.paddleLeftPoint.x + contactDelta.x * ((Pong.PADDLE_RADIUS + Pong.BALL_SIZE) * this.frame_height);
232                        newY = this.paddleLeftPoint.y + contactDelta.y * ((Pong.PADDLE_RADIUS + Pong.BALL_SIZE) * this.frame_height);
233
234                        final double rotation = -Math.atan2(contactDelta.y, contactDelta.x);
235                        final Matrix trans = TransformUtilities.rotationMatrix(rotation);
236                        final Point2dImpl newVel = this.ballVelocity.transform(trans);
237                        newVel.x *= -1;
238                        newVel.transform(trans.inverse());
239                        this.ballVelocity = newVel;
240
241                        // newX += ballVelocity.x;
242
243                        this.ballVelocity.x *= Pong.SPEED_MULTIPLIER;
244                        this.ballVelocity.y *= Pong.SPEED_MULTIPLIER;
245                }
246
247                if (dRightPaddle <= this.paddleRight.getRadius() + this.ball.getRadius()) {
248                        // contactPoint = contactPoint(paddleRight,this.ball);
249                        final Point2dImpl contactDelta = this.normContactDelta(this.paddleRight, this.ball);
250
251                        newX = this.paddleRightPoint.x + contactDelta.x * ((Pong.PADDLE_RADIUS + Pong.BALL_SIZE) * this.frame_height);
252                        newY = this.paddleRightPoint.y + contactDelta.y * ((Pong.PADDLE_RADIUS + Pong.BALL_SIZE) * this.frame_height);
253                        final double rotation = Math.atan2(contactDelta.y, -contactDelta.x);
254
255                        final Matrix trans = TransformUtilities.rotationMatrix(rotation);
256                        final Point2dImpl newVel = this.ballVelocity.transform(trans);
257                        newVel.x *= -1;
258                        newVel.transform(trans.inverse());
259                        this.ballVelocity = newVel;
260
261                        // newX += ballVelocity.x;
262
263                        this.ballVelocity.x *= Pong.SPEED_MULTIPLIER;
264                        this.ballVelocity.y *= Pong.SPEED_MULTIPLIER;
265                }
266
267                this.ballCentre.x = newX;
268                this.ballCentre.y = newY;
269        }
270
271        // private Point2dImpl contactPoint(final Circle paddle, final Circle ball)
272        // {
273        // final float px = paddle.getX();
274        // final float py = paddle.getY();
275        // final Point2dImpl contactDelta = this.normContactDelta(paddle,ball);
276        // return new Point2dImpl(px+contactDelta.x,py+contactDelta.y);
277        // }
278
279        private Point2dImpl normContactDelta(final Circle paddle, final Circle ball) {
280                final float px = paddle.getX();
281                final float py = paddle.getY();
282                final float dx = ball.getX() - px;
283                final float dy = ball.getY() - py;
284                final float plusX = dx * (paddle.getRadius() / (ball.getRadius() + paddle.getRadius()));
285                final float plusY = dy * (paddle.getRadius() / (ball.getRadius() + paddle.getRadius()));
286
287                final float prop = (float) (1f / Math.sqrt(plusX * plusX + plusY * plusY));
288                return new Point2dImpl(plusX * prop, plusY * prop);
289        }
290
291        @Override
292        public MBFImage getCurrentFrame() {
293                return this.lastFrame;
294        }
295
296        @Override
297        public int getWidth() {
298                return this.frame_width;
299        }
300
301        @Override
302        public int getHeight() {
303                return this.frame_height;
304        }
305
306        @Override
307        public long getTimeStamp() {
308                return (long) (this.currentFrame * 1000d / this.getFPS());
309        }
310
311        @Override
312        public double getFPS() {
313                return 30;
314        }
315
316        @Override
317        public boolean hasNextFrame() {
318                return true;
319        }
320
321        @Override
322        public long countFrames() {
323                return -1;
324        }
325
326        @Override
327        public void reset() {
328                this.initMatch();
329                this.getNextFrame();
330        }
331
332        public void leftPaddle(final float paddleY) {
333                this.paddleLeftPoint.y = paddleY;
334
335                if (this.paddleLeftPoint.y > this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.PADDLE_RADIUS))
336                        this.paddleLeftPoint.y = this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.PADDLE_RADIUS);
337                if (this.paddleLeftPoint.y < (Pong.BORDER_TOP + Pong.PADDLE_RADIUS) * this.frame_height)
338                        this.paddleLeftPoint.y = (Pong.BORDER_TOP + Pong.PADDLE_RADIUS) * this.frame_height;
339
340        }
341
342        protected void rightPaddle(final float paddleY) {
343                this.paddleRightPoint.y = paddleY;
344
345                if (this.paddleRightPoint.y < (Pong.BORDER_TOP + Pong.PADDLE_RADIUS) * this.frame_height)
346                        this.paddleRightPoint.y = (Pong.BORDER_TOP + Pong.PADDLE_RADIUS) * this.frame_height;
347                if (this.paddleRightPoint.y > this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.PADDLE_RADIUS))
348                        this.paddleRightPoint.y = this.frame_height * (1 - Pong.BORDER_BOTTOM - Pong.PADDLE_RADIUS);
349        }
350
351        public void leftPaddleUp() {
352                this.leftPaddle(this.paddleLeftPoint.y - Pong.PADDLE_VELOCITY * this.frame_height);
353        }
354
355        public void leftPaddleDown() {
356                this.leftPaddle(this.paddleLeftPoint.y + Pong.PADDLE_VELOCITY * this.frame_height);
357        }
358
359        protected void rightPaddleUp() {
360                this.rightPaddle(this.paddleRightPoint.y - Pong.PADDLE_VELOCITY * this.frame_height);
361        }
362
363        protected void rightPaddleDown() {
364                this.rightPaddle(this.paddleRightPoint.y + Pong.PADDLE_VELOCITY * this.frame_height);
365        }
366
367        public static void main(final String[] args) {
368                final Pong p = new Pong(640, 480);
369                final VideoDisplay<MBFImage> display = VideoDisplay.createVideoDisplay(p);
370                ((JFrame) SwingUtilities.getRoot(display.getScreen())).setTitle("OpenIMAJ Pong!");
371
372                SwingUtilities.getRoot(display.getScreen()).addKeyListener(new KeyListener() {
373
374                        @Override
375                        public void keyTyped(final KeyEvent e) {
376                        }
377
378                        @Override
379                        public void keyPressed(final KeyEvent e) {
380                                switch (e.getKeyChar()) {
381                                case 'a':
382                                        p.leftPaddleUp();
383                                        break;
384                                case 'z':
385                                        p.leftPaddleDown();
386                                        break;
387                                case '\'':
388                                        p.rightPaddleUp();
389                                        break;
390                                case '/':
391                                        p.rightPaddleDown();
392                                        break;
393                                case 'r':
394                                        p.reset();
395                                        break;
396                                }
397                        }
398
399                        @Override
400                        public void keyReleased(final KeyEvent e) {
401                        }
402
403                });
404        }
405
406        public float leftPaddleY() {
407                return this.paddleLeftPoint.y;
408        }
409
410        public float rightPaddleY() {
411                return this.paddleRightPoint.y;
412        }
413}