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.maze;
19  
20  import org.vectomatic.dom.svg.OMNode;
21  import org.vectomatic.dom.svg.OMSVGDocument;
22  import org.vectomatic.dom.svg.OMSVGGElement;
23  import org.vectomatic.dom.svg.OMSVGLength;
24  import org.vectomatic.dom.svg.OMSVGPathElement;
25  import org.vectomatic.dom.svg.OMSVGRect;
26  import org.vectomatic.dom.svg.OMSVGSVGElement;
27  import org.vectomatic.dom.svg.OMSVGStyleElement;
28  import org.vectomatic.dom.svg.OMText;
29  import org.vectomatic.dom.svg.ui.SVGPushButton;
30  import org.vectomatic.dom.svg.utils.AsyncXmlLoader;
31  import org.vectomatic.dom.svg.utils.AsyncXmlLoaderCallback;
32  import org.vectomatic.dom.svg.utils.SVGConstants;
33  import org.vectomatic.svg.edu.client.commons.CommonBundle;
34  import org.vectomatic.svg.edu.client.commons.CommonConstants;
35  import org.vectomatic.svg.edu.client.commons.DifficultyPicker;
36  import org.vectomatic.svg.edu.client.commons.LicenseBox;
37  import org.vectomatic.svg.edu.client.commons.Utils;
38  
39  import com.google.gwt.core.client.EntryPoint;
40  import com.google.gwt.core.client.GWT;
41  import com.google.gwt.core.client.JavaScriptObject;
42  import com.google.gwt.core.client.Scheduler;
43  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
44  import com.google.gwt.dom.client.Document;
45  import com.google.gwt.dom.client.NativeEvent;
46  import com.google.gwt.event.dom.client.ClickEvent;
47  import com.google.gwt.event.dom.client.KeyCodes;
48  import com.google.gwt.event.dom.client.KeyDownEvent;
49  import com.google.gwt.event.dom.client.MouseDownEvent;
50  import com.google.gwt.event.logical.shared.ValueChangeEvent;
51  import com.google.gwt.uibinder.client.UiBinder;
52  import com.google.gwt.uibinder.client.UiField;
53  import com.google.gwt.uibinder.client.UiHandler;
54  import com.google.gwt.user.client.Element;
55  import com.google.gwt.user.client.Timer;
56  import com.google.gwt.user.client.Window;
57  import com.google.gwt.user.client.ui.FlowPanel;
58  import com.google.gwt.user.client.ui.FocusPanel;
59  import com.google.gwt.user.client.ui.RootPanel;
60  import com.google.gwt.user.client.ui.Widget;
61  
62  /**
63   * Main class of the maze game
64   */
65  public class MazeMain implements EntryPoint {
66  	private static final String DIR = "maze";
67  	private static final String ID_MAZE = "maze";
68  
69  	interface MazeMainBinder extends UiBinder<FlowPanel, MazeMain> {
70  	}
71  	private static MazeMainBinder mainBinder = GWT.create(MazeMainBinder.class);
72  	
73  	@UiField(provided=true)
74  	static MazeBundle resources = MazeBundle.INSTANCE;
75  	@UiField(provided=true)
76  	static MazeLayoutCss mazeLayoutCss = MazeBundle.INSTANCE.mazeLayout();
77  	@UiField(provided=true)
78  	static CommonBundle common = CommonBundle.INSTANCE;
79  	static MazeCss style = resources.getCss();
80  	
81  	OMSVGDocument document;
82  	/**
83  	 * Root element of the maze SVG document
84  	 */
85  	OMSVGSVGElement svgRoot;
86  	/**
87  	 * The path which delimits the region where the maze will be generated.
88  	 * It also contains attributes describing the maze characteristics
89  	 */
90  	OMSVGPathElement mazeDef;
91  	/**
92  	 * A group of maze cells
93  	 */
94  	OMSVGGElement cellGroup;
95  	/**
96  	 * A path to represent the external boundaries of the maze
97  	 */
98  	OMSVGPathElement borderPath;
99  	/**
100 	 * A path to represent walls between maze cells
101 	 */
102 	OMSVGPathElement wallPath;
103 	
104 	@UiField
105 	FocusPanel focusPanel;
106 	@UiField
107 	SVGPushButton generateButton;
108 	@UiField
109 	SVGPushButton leftButton;
110 	@UiField
111 	SVGPushButton rightButton;
112 	@UiField
113 	SVGPushButton upButton;
114 	@UiField
115 	SVGPushButton downButton;
116 	@UiField
117 	SVGPushButton backButton;
118 	@UiField
119 	SVGPushButton helpButton;
120 	@UiField
121 	SVGPushButton prevButton;
122 	@UiField
123 	SVGPushButton nextButton;
124 	@UiField
125 	DifficultyPicker difficultyPicker;
126 	@UiField
127 	FlowPanel navigationPanel;
128 	Widget menuWidget;
129 	/**
130 	 * Game SVG level definitions
131 	 */
132 	String[] levels;
133 	/**
134 	 * Current index in the levels arrays
135 	 */
136 	int level;
137 	/**
138 	 * A timer to make the current position in the maze blink 
139 	 */
140 	static Timer positionTimer;
141 	/**
142 	 * A timer to flash the maze solution when the user requests it
143 	 */
144 	static Timer solutionTimer;
145 	/**
146 	 * The CSS rule which governs the color of the user path 
147 	 * in the maze (use for the color animation when the player wins)
148 	 */
149 	static JavaScriptObject pathRule;
150 	/**
151 	 * Use to free the UI at specific times (when displaying help,
152 	 * when loading a new level, ...)
153 	 */
154 	boolean frozen;
155 	/**
156 	 * The maze model
157 	 */
158 	RectangularMaze maze;
159 	
160 	/**
161 	 * To load game levels
162 	 */
163 	AsyncXmlLoader loader;
164 	
165 	/**
166 	 * Constructor for standalone game
167 	 */
168 	public MazeMain() {		
169 	}
170 	/**
171 	 * Constructor for integration in a menu
172 	 */
173 	public MazeMain(Widget menuWidget) {
174 		this.menuWidget = menuWidget;
175 	}
176 	
177 	/**
178 	 * Entry point
179 	 */
180 	@Override
181 	public void onModuleLoad() {
182 		// Inject styles from the common modules, taking into account media queries
183 		common.css().ensureInjected();
184 		common.mediaQueries().ensureInjected();
185 		Utils.injectMediaQuery("(orientation:landscape)", common.mediaQueriesLandscape());
186 		Utils.injectMediaQuery("(orientation:portrait)", common.mediaQueriesPortrait());
187 		Utils.injectMediaQuery("(orientation:landscape)", resources.mazeLayoutLandscape());
188 		Utils.injectMediaQuery("(orientation:portrait)", resources.mazeLayoutPortrait());
189 		style.ensureInjected();
190 		mazeLayoutCss.ensureInjected();
191 		
192 		if (solutionTimer != null) {
193 			solutionTimer.cancel();
194 		}
195 
196 		// Load the game levels
197 		levels = resources.levels().getText().split("\\s");
198 		loader = GWT.create(AsyncXmlLoader.class);
199 		
200 		// Initialize the UI with UiBinder
201 		FlowPanel panel = mainBinder.createAndBindUi(this);
202 		if (menuWidget == null) {
203 			menuWidget = LicenseBox.createAboutButton();
204 		}
205 		navigationPanel.insert(menuWidget, 0);
206 	
207 		int difficulty = 0;
208 		String difficultyParam = Window.Location.getParameter("difficulty");
209 		if (difficultyParam != null) {
210 			try {
211 				difficulty = Integer.parseInt(difficultyParam);	
212 			} catch(NumberFormatException e) {
213 			}
214 		}
215 		difficultyPicker.setDifficulty(difficulty);
216 		RootPanel.get(CommonConstants.ID_UIROOT).add(panel);
217 		
218 		String levelParam = Window.Location.getParameter("level");
219 		if (levelParam != null) {
220 			try {
221 				int value = Integer.parseInt(levelParam);
222 				if (value >= 0 && value < levels.length) {
223 					level = value;
224 				}
225 			} catch(NumberFormatException e) {
226 				GWT.log("Cannot parse level=" + levelParam, e);
227 			}
228 		}
229 		readMazeDef();
230 	}
231 	
232 	@UiHandler("generateButton")
233 	public void generate(ClickEvent event) {
234 		maze.perfectRandomize();
235 		if (solutionTimer != null) {
236 			solutionTimer.cancel();
237 		}
238 		String dumpParam = Window.Location.getParameter("dump");
239 		if ((!GWT.isScript()) && (dumpParam != null)) {
240 			OMSVGSVGElement root2 = (OMSVGSVGElement)svgRoot.cloneNode(true);
241 			OMSVGRect viewBox = root2.getViewBox().getBaseVal();
242 			if (viewBox.getWidth() <= viewBox.getHeight()) {
243 				root2.setWidth(OMSVGLength.SVG_LENGTHTYPE_CM, 21f);
244 				root2.setHeight(OMSVGLength.SVG_LENGTHTYPE_CM, 29.7f);
245 			} else {
246 				root2.setWidth(OMSVGLength.SVG_LENGTHTYPE_CM, 29.7f);
247 				root2.setHeight(OMSVGLength.SVG_LENGTHTYPE_CM, 21f);
248 			}
249 			OMSVGStyleElement styleElement = new OMSVGStyleElement();
250 			styleElement.setType(SVGConstants.CSS_TYPE);
251 			styleElement.appendChild(new OMText(style.getText()));
252 			root2.insertBefore(styleElement, root2.getFirstChild());
253 			
254 			GWT.log(root2.getMarkup());
255 		}
256 		setFillProperty(pathRule, SVGConstants.CSS_LIGHTGREEN_VALUE);
257 		update();
258 	}
259 	
260 	void clear() {
261 		if (positionTimer != null) {
262 			positionTimer.cancel();
263 		}
264 		OMSVGGElement g = new OMSVGGElement();
265 		if (cellGroup != null) {
266 			svgRoot.replaceChild(g, cellGroup);
267 		} else {
268 			svgRoot.appendChild(g);
269 		}
270 		cellGroup = g;
271 
272 		String wallStroke = mazeDef.getAttributeNS(RectangularMaze.VECTOMATIC_NS, RectangularMaze.WALL_TAG);
273 		OMSVGPathElement p = new OMSVGPathElement();
274 		p.setClassNameBaseVal(style.wall());
275 		p.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY, wallStroke);
276 		if (wallPath != null) {
277 			svgRoot.replaceChild(p, wallPath);
278 		} else {
279 			svgRoot.appendChild(p);
280 		}
281 		wallPath = p;
282 
283 		String borderStroke = mazeDef.getAttributeNS(RectangularMaze.VECTOMATIC_NS, RectangularMaze.BORDER_TAG);
284 		p = new OMSVGPathElement();
285 		p.setClassNameBaseVal(style.border());
286 		p.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY, borderStroke);
287 		if (borderPath != null) {
288 			svgRoot.replaceChild(p, borderPath);
289 		} else {
290 			svgRoot.appendChild(p);
291 		}
292 		borderPath = p;
293 
294 		String[] res = mazeDef.getAttributeNS(RectangularMaze.VECTOMATIC_NS, RectangularMaze.RES_TAG + (difficultyPicker.getDifficulty() + 1)).split("x");
295 		int colCount = Integer.parseInt(res[0]);
296 		int rowCount = Integer.parseInt(res[1]);
297 		maze = RectangularMaze.createMaze(colCount, rowCount, document, mazeDef, cellGroup, borderPath, wallPath);
298 		// A timer to make the current position blink
299 		positionTimer = new Timer() {
300 			@Override
301 			public void run() {
302 				if (maze != null) {
303 					maze.updateCurrent();
304 				}
305 			}
306 		};
307 		positionTimer.scheduleRepeating(250);
308 	}
309 	
310 	@UiHandler("leftButton")
311 	public void left(MouseDownEvent event) {
312 		maze.left();
313 		update();
314 	}
315 	@UiHandler("rightButton")
316 	public void right(MouseDownEvent event) {
317 		maze.right();
318 		update();
319 	}
320 	@UiHandler("upButton")
321 	public void up(MouseDownEvent event) {
322 		maze.up();
323 		update();
324 	}
325 	@UiHandler("downButton")
326 	public void down(MouseDownEvent event) {
327 		maze.down();
328 		update();
329 	}
330 	@UiHandler("backButton")
331 	public void backButton(MouseDownEvent event) {
332 		maze.back();
333 		update();
334 	}
335 	private void update() {
336 		frozen = false;
337 		difficultyPicker.setEnabled(true);
338 		leftButton.setEnabled(maze.canGoLeft());
339 		rightButton.setEnabled(maze.canGoRight());
340 		upButton.setEnabled(maze.canGoUp());
341 		downButton.setEnabled(maze.canGoDown());
342 		backButton.setEnabled(maze.canGoBack());
343 		Scheduler.get().scheduleDeferred(new ScheduledCommand() {
344 			@Override
345 			public void execute() {
346 				// Do this asynchronously while the button
347 				// is no longer the focus
348 				focusPanel.setFocus(true);
349 			}
350 		});
351 		if (maze.gameWon()) {
352 			freeze();
353 			// Retrieve the CSS rule governing the path color
354 			// Animate it to show the game has been won.
355 			if (pathRule == null) {
356 				pathRule = getRule("." + style.path());
357 				GWT.log(pathRule.toString());
358 			}
359 			solutionTimer = new Timer() {
360 				private int H = 120, S = 40, V = 93;
361 				@Override
362 				public void run() {
363 					H += 7;
364 					H = H % 360;
365 					int R = 0, G = 0, B = 0;
366                     int h = (H / 60);
367                     int p = (255 * V * (100 - S)) / 10000;
368                     int q = (255 * V * (6000 - S * (H - 60 * h))) / 600000;
369                     int t = (255 * V * (6000 - S * (60 - (H - 60 * h)))) / 600000;
370                     switch(h) {
371                             case 0:
372                                     R = V * 255 / 100;
373                                     G  = t;
374                                     B  = p;
375                                     break;
376                             case 1:
377                                     R = q;
378                                     G  = V * 255 / 100;
379                                     B  = p;
380                                     break;
381                             case 2:
382                                     R = p;
383                                     G  = V * 255 / 100;
384                                     B  = t;
385                                     break;
386                             case 3:
387                                     R = p;
388                                     G  = q;
389                                     B  = V * 255 / 100;
390                                     break;
391                             case 4:
392                                     R = t;
393                                     G  = p;
394                                     B  = V * 255 / 100;
395                                     break;
396                             case 5:
397                                     R = V * 255 / 100;
398                                     G = p;
399                                     B  = q;
400                                     break;
401                     }
402                     setFillProperty(pathRule, "rgb(" + R + "," + G + "," + B +")");
403 				}
404 
405 			};
406 			solutionTimer.scheduleRepeating(50);
407 		}
408 	}
409 	private static final native JavaScriptObject getRule(String selector) /*-{
410 	  for (var i = 0; i < $doc.styleSheets.length; i++) {
411 	    var stylesheet = $doc.styleSheets[i];
412 	    for (var j = 0; j < stylesheet.cssRules.length; j++) {
413 	      var rule = stylesheet.cssRules[j];
414 	      if (rule.selectorText == selector) {
415 	        return rule;
416 	      }
417 	    }
418 	  }
419 	  return null;
420 	}-*/;
421 	
422 	private static final native void setFillProperty(JavaScriptObject rule, String color) /*-{
423 	  if (rule != null) {
424 	    rule.style.setProperty('fill', color, '');
425 	  }
426 	}-*/;
427 	
428 	private void freeze() {
429 		helpButton.setEnabled(false);
430 		difficultyPicker.setEnabled(false);
431 		leftButton.setEnabled(false);
432 		rightButton.setEnabled(false);
433 		upButton.setEnabled(false);
434 		downButton.setEnabled(false);
435 		backButton.setEnabled(false);
436 		frozen = true;
437 	}
438 
439 	@UiHandler("helpButton")
440 	public void help(ClickEvent event) {
441 		freeze();
442 		generateButton.setEnabled(false);
443 		maze.displaySolution(true);
444 		Timer solutionTimer = new Timer() {
445 			@Override
446 			public void run() {
447 				maze.displaySolution(false);
448 				helpButton.setEnabled(true);
449 				generateButton.setEnabled(true);
450 				difficultyPicker.setEnabled(true);
451 				update();
452 			}
453 		};
454 		solutionTimer.schedule(3000);
455 	}
456 	@UiHandler("prevButton")
457 	public void prevButton(ClickEvent event) {
458 		level--;
459 		if (level < 0) {
460 			level = levels.length - 1;
461 		}
462 		readMazeDef();
463 	}
464 	@UiHandler("nextButton")
465 	public void nextButton(ClickEvent event) {
466 		level++;
467 		if (level >= levels.length) {
468 			level = 0;
469 		}
470 		readMazeDef();
471 	}
472 	
473 	private static final native int eventGetKeyCode(NativeEvent evt) /*-{
474 	  // 'which' gives the right key value, except when it doesn't -- in which
475 	  // case, keyCode gives the right value on all browsers.
476 	  // If all else fails, return an error code
477 	  return evt.which || evt.keyCode || 0;
478 	}-*/;
479 	  
480 	@UiHandler("focusPanel")
481 	public void onKeyDown(KeyDownEvent event) {
482 		if (!frozen) {
483 			int code = eventGetKeyCode(event.getNativeEvent());
484 			switch (code) {
485 				case KeyCodes.KEY_DOWN:
486 					maze.down();
487 					break;
488 				case KeyCodes.KEY_RIGHT:
489 					maze.right();
490 					break;
491 				case KeyCodes.KEY_UP:
492 					maze.up();
493 					break;
494 				case KeyCodes.KEY_LEFT:
495 					maze.left();
496 					break;
497 				case ' ':
498 					maze.back();
499 					break;
500 				default:
501 					//GWT.log("key code:" + (int)event.getCharCode() + " " + (int)event.getUnicodeCharCode());
502 					GWT.log("key code:" + (int)event.getNativeKeyCode());
503 			}
504 			update();
505 		}
506 	}
507 	
508 	@UiHandler("difficultyPicker")
509 	public void levelChange(ValueChangeEvent<Integer> event) {
510 		clear();
511 		generate(null);
512 	}
513 	
514 	private void displayMazeDef(OMSVGSVGElement svg) {
515 		// Add the SVG to the HTML page
516 		Element div = focusPanel.getElement();
517 		if (svgRoot != null) {
518 			div.replaceChild(svg.getElement(), svgRoot.getElement());
519 		} else {
520 			div.appendChild(svg.getElement());					
521 		}
522 		svgRoot = svg;
523 		document = (OMSVGDocument) svgRoot.getOwnerDocument();
524 		mazeDef = (OMSVGPathElement) document.getElementById(ID_MAZE);
525 		cellGroup = null;
526 		wallPath = null;
527 		borderPath = null;
528 		clear();
529 		generate(null);		
530 	}
531 
532 	public void readMazeDef() {
533 		String url = GWT.getModuleBaseURL() + DIR + "/" + levels[level];
534 		loader.loadResource(url, new AsyncXmlLoaderCallback() {
535 			@Override
536 			public void onError(String resourceName, Throwable error) {
537 				focusPanel.getElement().appendChild(Document.get().createTextNode("Cannot find resource"));
538 			}
539 
540 			@Override
541 			public void onSuccess(String resourceName, com.google.gwt.dom.client.Element root) {
542 				OMSVGSVGElement svg = OMNode.convert(root);
543 				displayMazeDef(svg);
544 			}
545 		});
546 	}
547 }