View Javadoc

1   /**********************************************
2    * Copyright (C) 2010 Lukas Laag
3    * This file is part of lib-gwt-svg-edu.
4    * 
5    * libgwtsvg-edu is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    * 
10   * libgwtsvg-edu is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   * 
15   * You should have received a copy of the GNU General Public License
16   * along with libgwtsvg-edu.  If not, see http://www.gnu.org/licenses/
17   **********************************************/
18  package org.vectomatic.svg.edu.client.push;
19  
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import org.vectomatic.dom.svg.OMNode;
24  import org.vectomatic.dom.svg.OMSVGClipPathElement;
25  import org.vectomatic.dom.svg.OMSVGDefsElement;
26  import org.vectomatic.dom.svg.OMSVGGElement;
27  import org.vectomatic.dom.svg.OMSVGMatrix;
28  import org.vectomatic.dom.svg.OMSVGPoint;
29  import org.vectomatic.dom.svg.OMSVGRect;
30  import org.vectomatic.dom.svg.OMSVGRectElement;
31  import org.vectomatic.dom.svg.OMSVGSVGElement;
32  import org.vectomatic.dom.svg.OMSVGTransform;
33  import org.vectomatic.dom.svg.OMSVGTransformList;
34  import org.vectomatic.dom.svg.OMSVGUseElement;
35  import org.vectomatic.dom.svg.ui.SVGPushButton;
36  import org.vectomatic.dom.svg.utils.AsyncXmlLoader;
37  import org.vectomatic.dom.svg.utils.AsyncXmlLoaderCallback;
38  import org.vectomatic.dom.svg.utils.SVGConstants;
39  import org.vectomatic.svg.edu.client.commons.CommonBundle;
40  import org.vectomatic.svg.edu.client.commons.CommonConstants;
41  import org.vectomatic.svg.edu.client.commons.DifficultyPicker;
42  import org.vectomatic.svg.edu.client.commons.LicenseBox;
43  import org.vectomatic.svg.edu.client.commons.Utils;
44  
45  import com.google.gwt.animation.client.Animation;
46  import com.google.gwt.core.client.Duration;
47  import com.google.gwt.core.client.EntryPoint;
48  import com.google.gwt.core.client.GWT;
49  import com.google.gwt.dom.client.Style.Unit;
50  import com.google.gwt.dom.client.StyleInjector;
51  import com.google.gwt.event.dom.client.ClickEvent;
52  import com.google.gwt.event.dom.client.MouseDownEvent;
53  import com.google.gwt.event.dom.client.MouseDownHandler;
54  import com.google.gwt.event.dom.client.MouseEvent;
55  import com.google.gwt.event.logical.shared.ValueChangeEvent;
56  import com.google.gwt.event.shared.EventHandler;
57  import com.google.gwt.uibinder.client.UiBinder;
58  import com.google.gwt.uibinder.client.UiField;
59  import com.google.gwt.uibinder.client.UiHandler;
60  import com.google.gwt.user.client.Command;
61  import com.google.gwt.user.client.DeferredCommand;
62  import com.google.gwt.user.client.Element;
63  import com.google.gwt.user.client.Random;
64  import com.google.gwt.user.client.Timer;
65  import com.google.gwt.user.client.Window;
66  import com.google.gwt.user.client.ui.FlowPanel;
67  import com.google.gwt.user.client.ui.HTML;
68  import com.google.gwt.user.client.ui.RootPanel;
69  import com.google.gwt.user.client.ui.Widget;
70  
71  public class PushMain implements MouseDownHandler, EntryPoint {
72  	private static final String DIR = "push";
73  	private static final String ID_CLIP_PATH = "cp";
74  	private static final String ID_CLIP_RECT = "cpr";
75  	private static final String ID_IMAGE = "puzzle";
76  	private static final String ID_TILE = "t";
77  	private static final int MARGIN = 3;
78  	
79  	PushBundle resources = PushBundle.INSTANCE;
80  	@UiField(provided=true)
81  	CommonBundle common = CommonBundle.INSTANCE;
82  	PushCss style = resources.getCss();
83  	@UiField
84  	SVGPushButton prevButton;
85  	@UiField
86  	SVGPushButton nextButton;
87  	@UiField
88  	HTML svgContainer;
89  	@UiField
90  	DifficultyPicker difficultyPicker;
91  	@UiField
92  	FlowPanel navigationPanel;
93  	Widget menuWidget;
94  
95  	/**
96  	 * To load game levels
97  	 */
98  	AsyncXmlLoader loader;
99  	private String[] levels;
100 	private int currentLevel;
101 	/**
102 	 * The source image svg element
103 	 */
104 	private OMSVGSVGElement srcSvg;
105 	/**
106 	 * The puzzle root svg element
107 	 */
108 	private OMSVGSVGElement pushSvg;
109 	/**
110 	 * BBox of the puzzle image
111 	 */
112 	private OMSVGRect bbox;
113 	/**
114 	 * A matrix of tile ids reflecting the game state
115 	 */
116 	private int[][] game;
117 	/**
118 	 *  The tile id of the tile representing the hole
119 	 */
120 	private int hole;
121 	/**
122 	 * Width of the puzzle border
123 	 */
124 	int xcount;
125 	/**
126 	 * Number of puzzle tiles in a column
127 	 */
128 	int ycount;
129 	/**
130 	 * The game tiles (upper left has index 0,
131 	 * lower right has coordinate length - 1)
132 	 * There are xcount * ycount -1 tiles (-1
133 	 * is for the hole)
134 	 */
135 	private OMSVGUseElement[] tiles;
136 	/**
137 	 * True when the game has begun
138 	 */
139 	private boolean playing;
140 	/**
141 	 * Use to free the UI at specific times (when displaying help,
142 	 * when loading a new level, ...)
143 	 */
144 	boolean frozen;
145 	/**
146 	 * Timer to briefly display the puzzle assembled
147 	 */
148 	private Timer waitTimer;
149 	/**
150 	 * Time to time scrambles apart
151 	 */
152 	private Timer scrambleTimer;
153 	/**
154 	 * Class to display an animation when the player wins
155 	 */
156 	private int animCount;
157 	private Animation animation = new Animation() {
158 		@Override
159 		protected void onUpdate(double progress) {
160 			for (int i = 0; i < xcount; i++) {
161 				for (int j = 0; j < ycount; j++) {
162 					int index = i * ycount + j;
163 					if (index != hole) {
164 						OMSVGTransformList xformList = tiles[index].getTransform().getBaseVal();
165 						xformList.clear();
166 						OMSVGTransform r = pushSvg.createSVGTransform();
167 						r.setRotate((float)(360 * progress * ((animCount % 2 == 0) ? 1f : -1f)), (i + 0.5f) * bbox.getWidth() / xcount, (j + 0.5f) * bbox.getHeight() / ycount);
168 						xformList.appendItem(r);
169 					}
170 				}
171 			}
172 		}
173 		@Override
174 		protected void onComplete() {
175 			super.onComplete();
176 			animCount++;
177 			if (animCount < 5) {
178 				DeferredCommand.addCommand(new Command() {
179 					@Override
180 					public void execute() {
181 						animation.run(1000, Duration.currentTimeMillis() + 500);
182 					}					
183 				});
184 			} else {
185 				playing = true;
186 				GWT.log(getDescription());
187 			}
188 		}
189 	};
190 	
191 	interface PushMainBinder extends UiBinder<FlowPanel, PushMain> {
192 	}
193 	private static PushMainBinder mainBinder = GWT.create(PushMainBinder.class);
194 	
195 	/**
196 	 * Constructor for standalone game
197 	 */
198 	public PushMain() {
199 	}
200 	/**
201 	 * Constructor for integration in a menu
202 	 */
203 	public PushMain(Widget menuWidget) {
204 		this.menuWidget = menuWidget;
205 	}
206 	
207 	/**
208 	 * Entry point when the game is run in standalone mode
209 	 */
210 	@Override
211 	public void onModuleLoad() {
212 		// Inject styles from the common modules, taking into account media queries
213 		common.css().ensureInjected();
214 		common.mediaQueries().ensureInjected();
215 		Utils.injectMediaQuery("(orientation:landscape)", common.mediaQueriesLandscape());
216 		Utils.injectMediaQuery("(orientation:portrait)", common.mediaQueriesPortrait());
217 		StyleInjector.inject(style.getText(), true);
218 		
219 		// Load the game levels
220 		levels = resources.levels().getText().split("\\s");
221 		loader = GWT.create(AsyncXmlLoader.class);
222 		
223 		// Initialize the UI with UiBinder
224 		FlowPanel panel = mainBinder.createAndBindUi(this);
225 		if (menuWidget == null) {
226 			menuWidget = LicenseBox.createAboutButton();
227 		}
228 		navigationPanel.insert(menuWidget, 0);
229 		RootPanel.get(CommonConstants.ID_UIROOT).add(panel);
230 		readPushDef();
231 	}
232 	
233 	@UiHandler("prevButton")
234 	public void prevButton(ClickEvent event) {
235 		currentLevel--;
236 		if (currentLevel < 0) {
237 			currentLevel = levels.length - 1;
238 		}
239 		readPushDef();
240 	}
241 	@UiHandler("nextButton")
242 	public void nextButton(ClickEvent event) {
243 		currentLevel++;
244 		if (currentLevel >= levels.length) {
245 			currentLevel = 0;
246 		}
247 		readPushDef();
248 	}
249 
250 	@UiHandler("difficultyPicker")
251 	public void levelChange(ValueChangeEvent<Integer> event) {
252 		generate();
253 	}
254 	
255 	private void generate() {
256 		OMSVGSVGElement rootSvg = new OMSVGSVGElement();
257 		rootSvg.addClassNameBaseVal(style.rootSvg());
258 		rootSvg.addMouseDownHandler(this);
259 		OMSVGDefsElement defs = new OMSVGDefsElement();
260 		rootSvg.appendChild(defs);
261 		
262 		// Copy the source SVG in a dedicated group inside
263 		// the defs
264 		OMSVGGElement imgGroup = new OMSVGGElement();
265 		imgGroup.setId(ID_IMAGE);
266 		for (OMNode node : srcSvg.getChildNodes()) {
267 			imgGroup.appendChild(node.cloneNode(true));
268 		}
269 		defs.appendChild(imgGroup);
270 		
271 		OMSVGRect viewBox = srcSvg.getViewBox().getBaseVal();
272 		float width = viewBox.getWidth();
273 		float height = viewBox.getHeight();
274 		bbox = rootSvg.createSVGRect();
275 		viewBox.assignTo(bbox);
276 		
277 		// Compute the number of tiles
278 		if (width < height) {
279 			xcount = difficultyPicker.getDifficulty() + 3;
280 			ycount = (int)(xcount * height / width);
281 		} else {
282 			ycount = difficultyPicker.getDifficulty() + 3;
283 			xcount = (int)(ycount * width / height);
284 		}
285 		hole = xcount * ycount - 1;
286 		
287 		
288 		// Create a thick border with rounded corners around the
289 		// drawing (15% of the original drawing size, corner radius
290 		// 2.5% of the original drawing size)
291 		// Add a 3 pixel margin around the tiles
292 		float borderWidth = (int)(0.075f * width);
293 		float borderHeight = (int)(0.075f * height);
294 		float borderRx = (int)(0.025f * width);
295 		float borderRy = (int)(0.025f * height);
296 		OMSVGRectElement borderOut = new OMSVGRectElement(
297 				viewBox.getX() - borderWidth - MARGIN, 
298 				viewBox.getY() - borderHeight - MARGIN,
299 				viewBox.getWidth() + 2 * (borderWidth + MARGIN), 
300 				viewBox.getHeight() + 2 * (borderHeight + MARGIN), 
301 				borderRx,
302 				borderRy);
303 		borderOut.setClassNameBaseVal(style.borderOut());
304 		OMSVGRectElement borderIn = new OMSVGRectElement(
305 				viewBox.getX() - MARGIN, 
306 				viewBox.getY() - MARGIN,
307 				viewBox.getWidth() + 2 * MARGIN, 
308 				viewBox.getHeight() + 2 * MARGIN, 
309 				borderRx,
310 				borderRy);
311 		borderIn.setClassNameBaseVal(style.borderIn());
312 		rootSvg.appendChild(borderOut);
313 		rootSvg.appendChild(borderIn);
314 //		rootSvg.setWidth(OMSVGLength.SVG_LENGTHTYPE_PERCENTAGE, 65f);
315 //		rootSvg.setHeight(OMSVGLength.SVG_LENGTHTYPE_PERCENTAGE, 65f);
316 		rootSvg.setViewBox(
317 				viewBox.getX() - borderWidth - MARGIN, 
318 				viewBox.getY() - borderHeight - MARGIN,
319 				viewBox.getWidth() + 2 * (borderWidth + MARGIN), 
320 				viewBox.getHeight() + 2 * (borderHeight + MARGIN));
321 		rootSvg.getWidth().getBaseVal().newValueSpecifiedUnits(Unit.PCT, 100);
322 		rootSvg.getHeight().getBaseVal().newValueSpecifiedUnits(Unit.PCT, 100);
323 		
324 		// Create the tile clip-path
325 		// <clipPath id="cp">
326 	 	//  <rect id="cpr" x="0" y="0" width="130" height="130" rx="10" ry="10"/>
327 	 	// </clipPath>
328 		OMSVGClipPathElement clipPath = new OMSVGClipPathElement();
329 		clipPath.setId(ID_CLIP_PATH);
330 		OMSVGRectElement clipRect = new OMSVGRectElement(
331 				viewBox.getX(),
332 				viewBox.getY(),
333 				width / xcount,
334 				height / ycount,
335 				borderRx,
336 				borderRy);
337 		clipRect.setId(ID_CLIP_RECT);
338 		clipPath.appendChild(clipRect);
339 		defs.appendChild(clipPath);
340 		
341 		// Create the tiles
342 		tiles = new OMSVGUseElement[xcount * ycount];
343 		game = new int[xcount][];
344 		for (int i = 0; i < xcount; i++) {
345 			game[i] = new int[ycount];
346 			for (int j = 0; j < ycount; j++) {
347 				int index = i * ycount + j;
348 				if (index != hole) {
349 					// Create the tile definition
350 					// Each tile definition has the following structure
351 					// <g id="tileXXX">
352 					//  <g style="clip-path:url(#cp)">
353 					//   <g transform="translate(-260,0)">
354 					//    <use x="0" y="0" xlink:href="#puzzle"/>
355 					//   </g>
356 					//  </g>
357 					//  <use x="0" y="0" xlink:href="#cp1r" style="fill:none;stroke:black;"/>
358 					// </g>		
359 					OMSVGGElement tileDef = new OMSVGGElement();
360 					tileDef.setId(ID_TILE + index);
361 					OMSVGGElement tileClipPath = new OMSVGGElement();
362 					tileClipPath.getStyle().setSVGProperty(SVGConstants.CSS_CLIP_PATH_PROPERTY, "url(#" + ID_CLIP_PATH + ")");
363 					OMSVGGElement tileTransform = new OMSVGGElement();
364 					OMSVGTransform xform = rootSvg.createSVGTransform();
365 					xform.setTranslate(
366 							viewBox.getX() - i * width / xcount, 
367 							viewBox.getY() - j * height / ycount);
368 					tileTransform.getTransform().getBaseVal().appendItem(xform);
369 					OMSVGUseElement imgUse = new OMSVGUseElement();
370 					imgUse.getX().getBaseVal().setValue(viewBox.getX());
371 					imgUse.getY().getBaseVal().setValue(viewBox.getY());
372 					imgUse.getHref().setBaseVal("#" + ID_IMAGE);
373 					OMSVGUseElement tileBorder = new OMSVGUseElement();
374 					tileBorder.getX().getBaseVal().setValue(viewBox.getX());
375 					tileBorder.getY().getBaseVal().setValue(viewBox.getY());
376 					tileBorder.getHref().setBaseVal("#" + ID_CLIP_RECT);
377 					tileBorder.setClassNameBaseVal(style.tileBorder());
378 					tileDef.appendChild(tileClipPath);
379 					tileClipPath.appendChild(tileTransform);
380 					tileTransform.appendChild(imgUse);
381 					tileDef.appendChild(tileBorder);
382 					defs.appendChild(tileDef);
383 					
384 					// Create the tile itself
385 					// <use x="130" y="260" xlink:href="#tileXXX"/>
386 					tiles[index] = new OMSVGUseElement();
387 					tiles[index].getHref().setBaseVal("#" + ID_TILE + index);
388 					rootSvg.appendChild(tiles[index]);
389 				}
390 				setTile(i, j, index);
391 			}
392 		}
393 
394 		// Add the SVG to the HTML page
395 		Element div = svgContainer.getElement();
396 		if (pushSvg != null) {
397 			div.replaceChild(rootSvg.getElement(), pushSvg.getElement());
398 		} else {
399 			div.appendChild(rootSvg.getElement());					
400 		}
401 		pushSvg = rootSvg;
402 		if (!GWT.isScript()) {
403 			GWT.log(pushSvg.getMarkup());
404 		}
405 
406 		// Display the puzzle in order for 1 sec, then scramble it
407 		waitTimer = new Timer() {
408 			public void run() {
409 				scrambleTimer = new Timer() {
410 					int repeatCount;
411 					@Override
412 					public void run() {
413 						int tileCount = xcount * ycount;
414 						List<Integer> array = new ArrayList<Integer>();
415 						for (int i = 0; i < tileCount; i++) {
416 							array.add(i);
417 						}
418 						// Shuffle the tiles
419 						for (int i = 0; i < xcount; i++) {
420 							for (int j = 0; j < ycount; j++) {
421 								setTile(i, j, array.remove(Random.nextInt(tileCount--)));
422 							}
423 						}
424 						repeatCount++;
425 						if (repeatCount >= 5) {
426 							playing = true;
427 							cancel();
428 						}
429 					}
430 				};
431 				if ("true".equals(Window.Location.getParameter("win"))) {
432 					winAnimation();
433 				} else {
434 					scrambleTimer.scheduleRepeating(200);
435 				}
436 			}
437 		};
438 		waitTimer.schedule(1000);
439 	}
440 	
441 	public boolean gameOver() {
442 		for (int i = 0; i < xcount; i++) {
443 			for (int j = 0; j < ycount; j++) {
444 				if (!(getTile(i, j) == i * ycount + j)) {
445 					return false;
446 				}
447 			}
448 		}
449 		return true;
450 	}
451 	
452 	public void setTile(int x, int y, int value) {
453 		game[x][y] = value;
454 		if (value != hole) {
455 			tiles[value].getX().getBaseVal().setValue(x * bbox.getWidth() / xcount);
456 			tiles[value].getY().getBaseVal().setValue(y * bbox.getHeight() / ycount);
457 		}
458 	}
459 	public int getTile(int x, int y) {
460 		return game[x][y];
461 	}
462 	
463 	private String getDescription() {
464 		StringBuilder builder = new StringBuilder();
465 		for (int i = 0; i < xcount; i++) {
466 			builder.append("| ");
467 			for (int j = 0; j < ycount; j++) {
468 				builder.append(" ");
469 				builder.append(getTile(i, j));
470 			}
471 			builder.append(" |\n");
472 		}
473 		return builder.toString();
474 	}
475 
476 	@Override
477 	public void onMouseDown(MouseDownEvent event) {
478 		if (playing) {
479 			OMSVGPoint coords = getTileCoordinates(event);
480 			if (coords != null) {
481 				GWT.log("mouseDown: " + coords.getDescription());
482 				int x = (int) coords.getX();
483 				int y = (int) coords.getY();
484 				boolean shifted = false;
485 				for (int i = 0; i < xcount; i++) {
486 					if (game[i][y] == hole) {
487 						if (x < i) {
488 							for (int j = i; j > x; j--) {
489 								setTile(j, y, getTile(j - 1, y));
490 							}
491 						} else {
492 							for (int j = i; j < x; j++) {
493 								setTile(j, y, getTile(j + 1, y));
494 							}
495 						}
496 						setTile(x, y, hole);
497 						GWT.log(getDescription());
498 						shifted = true;
499 					}
500 				}
501 				if (!shifted) {
502 					for (int i = 0; i < ycount; i++) {
503 						if (game[x][i] == hole) {
504 							if (y < i) {
505 								for (int j = i; j > y; j--) {
506 									setTile(x, j, getTile(x, j - 1));
507 								}
508 							} else {
509 								for (int j = i; j < y; j++) {
510 									setTile(x, j, getTile(x, j + 1));
511 								}
512 							}
513 							setTile(x, y, hole);
514 							GWT.log(getDescription());
515 						}
516 					}
517 				}
518 				if (gameOver()) {
519 					winAnimation();
520 				}
521 				event.stopPropagation();
522 			}
523 		}
524 	}
525 	
526 	public OMSVGPoint getTileCoordinates(MouseEvent<? extends EventHandler> e) {
527 		OMSVGPoint p = pushSvg.createSVGPoint(e.getClientX(), e.getClientY());
528 		OMSVGMatrix m = pushSvg.getScreenCTM().inverse();
529 		p = p.matrixTransform(m).substract(pushSvg.createSVGPoint(bbox.getX(), bbox.getY())).product(pushSvg.createSVGPoint(xcount / bbox.getWidth(), ycount / bbox.getHeight())).floor();
530 		return pushSvg.createSVGRect(0, 0, xcount - 1, ycount - 1).contains(p) ? p : null;
531 	}
532 	
533 	public void winAnimation() {
534 		playing = false;
535 		animCount = 0;
536 		animation.run(1000, Duration.currentTimeMillis() + 500);
537 	}
538 
539 
540 	private String getLevelUrl() {
541 		return GWT.getModuleBaseURL() + DIR + "/" + levels[currentLevel];
542 	}
543 
544 	public void readPushDef() {
545 		playing = false;
546 		if (waitTimer != null) {
547 			waitTimer.cancel();
548 		}
549 		if (scrambleTimer != null) {
550 			scrambleTimer.cancel();
551 		}
552 		if (animation != null) {
553 			animation.cancel();
554 		}
555 		String url = GWT.getModuleBaseURL() + DIR + "/" + levels[currentLevel];
556 		loader.loadResource(url, new AsyncXmlLoaderCallback() {
557 			@Override
558 			public void onError(String resourceName, Throwable error) {
559 				svgContainer.setHTML("Cannot find resource");
560 			}
561 
562 			@Override
563 			public void onSuccess(String resourceName, com.google.gwt.dom.client.Element root) {
564 				srcSvg = OMNode.convert(root);
565 				generate();
566 			}
567 		});
568 	}
569 }
570