1 module snake.app;
2 
3 import pixelperfectengine.graphics.outputscreen;
4 import pixelperfectengine.graphics.raster;
5 import pixelperfectengine.graphics.layers;
6 
7 import pixelperfectengine.graphics.bitmap;
8 
9 import pixelperfectengine.system.input;
10 import pixelperfectengine.system.input.scancode;
11 import pixelperfectengine.system.file;
12 import pixelperfectengine.system.etc;
13 import pixelperfectengine.system.config;
14 import pixelperfectengine.system.timer;
15 import pixelperfectengine.system.rng;
16 
17 import pixelperfectengine.system.common;
18 /**
19  * The main entry point. Contains essential calls to initializing and running the program.
20  */
21 int main() {
22 	//SDL initialization call. Once the library `iota` is mature enough for input/output,
23 	//it'll be removed.
24 	initialzeSDL();
25 	//Initialize our game.
26 	SnakeGame game = new SnakeGame();
27 	//Run the game.
28 	game.whereTheMagicHappens();
29 	return 0;
30 }
31 /** 
32  * A simple snake game.
33  * Uses code-generated graphics to avoid relying on external assets.
34  *
35  * Game rules:
36  * * The player controls a snake.
37  * * The player must collect apples to get score, which also makes the snake grow.
38  * * Every time an apple is collected, a new one is randomly spawned on the map at a point outside of the snake.
39  * * If the player either hits the snake or wall, they lose.
40  * * If the player somehow makes the snake to grow so big a new apple cannot be spawned, then the player wins.
41  *
42  * Design guidelines:
43  * * No much external assets. (except for engine essentials).
44  * * Snake tail follows past directions
45  *
46  * Some possible assignments to practice using the engine:
47  * * Use some external assets.
48  * * Add some obstacles.
49  * * Add a second, third, etc. player.
50  */
51 public class SnakeGame : InputListener, SystemEventListener {
52 	/** 
53 	 * Used for direction tracing in the API.
54 	 */
55 	enum Direction : ubyte {
56 		init		=	0,
57 		North		=	1,
58 		South		=	2,
59 		East		=	4,
60 		West		=	8,
61 	}
62 	/** 
63 	 * Defines tile type codes.
64 	 *
65 	 * Can also be used to track the snake itself.
66 	 */
67 	enum TileTypes : wchar {
68 		init,
69 		Empty		=	0x00,
70 		Apple		=	0x01,
71 		SnakeH		=	0x11,
72 		SnakeV		=	0x12,
73 		SnakeNE		=	0x13,
74 		SnakeSE		=	0x14,
75 		SnakeNW		=	0x15,
76 		SnakeSW		=	0x16,
77 	}
78 	///The position of the head of the snake.
79 	///We can track the rest of the snake by tracking the directions.
80 	Point snakeHead;
81 	///Counts how much score the player has.
82 	int score;
83 	///Program and game state.
84 	///4: program exit
85 	///5: game start
86 	///6: game over
87 	///7: game won
88 	ubyte state;
89 	///Contains the current direction.
90 	ubyte dir;
91 	///Contains the previous direction.
92 	ubyte prevDir;
93 	///The output screen, where the output is being displayed.
94 	OutputScreen	output;
95 	///Handles all the inputs. On every keypress/buttonpress, a new event is created, which changes the internal state
96 	///of the program.
97 	InputHandler	ih;
98 	///The raster handles frame buffer and layer priority management. Even though we currently only have a single 
99 	///tilelayer, we will still needing it.
100 	Raster			raster;
101 	///The TileLayer, that:
102 	/// * Displays the current game state
103 	/// * Stores playfield data (snaketail, apple's position, etc)
104 	TileLayer		playfield;
105 	///These are the tiles, we're going to use in our game
106 	Bitmap8Bit		empty, apple, snakeH, snakeV, snakeNE, snakeSE, snakeNW, snakeSW;
107 	///A random number generator
108 	RandomNumberGenerator rng;
109 	///Constructor to initialize things.
110 	///It is important to keep initialization and game logic separate.
111 	public this() {
112 		//Create all the tiles needed for the game.
113 
114 		//The easiest is the empty tile, all we need is to create an empty bitmap (all indexes are 0).
115 		empty = new Bitmap8Bit(8,8);
116 		//The apple uses all red color. Initialize an empty bitmap, then set all the pixels to index 2.
117 		apple = new Bitmap8Bit(8,8);
118 		for (int y ; y < 8 ; y++) {	//Use some iterations to easily set all indexes. Top-left is the origin point, and indexing starts at 0.
119 			for (int x ; x < 8 ; x++) {
120 				//NOTE: it's faster to write to the bitmap by directly accessing its data, but for this purpose, it's
121 				//more than enough.
122 				apple.writePixel(x, y, 0x2);
123 			}
124 		}
125 		//The snakeH is a horizontal piece, and our snake is 4 pixels thick. Let's draw it with similar method!
126 		snakeH = new Bitmap8Bit(8,8);
127 		for (int y = 2 ; y < 6 ; y++) {
128 			for (int x ; x < 8 ; x++) {
129 				snakeH.writePixel(x, y, 0x1);
130 			}
131 		}
132 		//The snakeV is the straight vertical piece.
133 		snakeV = new Bitmap8Bit(8,8);
134 		for (int y ; y < 8 ; y++) {
135 			for (int x = 2 ; x < 6 ; x++) {
136 				snakeV.writePixel(x, y, 0x1);
137 			}
138 		}
139 		//The corner pieces are more complicated, but not too hard.
140 		//This piece is the north-east piece, it connects the top (north) with the right (east). The rest of the corner
141 		//tiles use the same system, so I won't annotate their creation.
142 		snakeNE = new Bitmap8Bit(8,8);
143 		for (int y ; y < 2 ; y++) {
144 			for (int x = 2 ; x < 6 ; x++) {
145 				snakeNE.writePixel(x, y, 0x1);
146 			}
147 		}
148 		for (int y = 2 ; y < 6 ; y++) {
149 			for (int x = 2 ; x < 8 ; x++) {
150 				snakeNE.writePixel(x, y, 0x1);
151 			}
152 		}
153 		snakeSE = new Bitmap8Bit(8,8);
154 		for (int y = 6 ; y < 8 ; y++) {
155 			for (int x = 2 ; x < 6 ; x++) {
156 				snakeSE.writePixel(x, y, 0x1);
157 			}
158 		}
159 		for (int y = 2 ; y < 6 ; y++) {
160 			for (int x = 2 ; x < 8 ; x++) {
161 				snakeSE.writePixel(x, y, 0x1);
162 			}
163 		}
164 		snakeNW = new Bitmap8Bit(8,8);
165 		for (int y ; y < 2 ; y++) {
166 			for (int x = 2 ; x < 6 ; x++) {
167 				snakeNW.writePixel(x, y, 0x1);
168 			}
169 		}
170 		for (int y = 2 ; y < 6 ; y++) {
171 			for (int x ; x < 6 ; x++) {
172 				snakeNW.writePixel(x, y, 0x1);
173 			}
174 		}
175 		snakeSW = new Bitmap8Bit(8,8);
176 		for (int y = 6 ; y < 8 ; y++) {
177 			for (int x = 2 ; x < 6 ; x++) {
178 				snakeSW.writePixel(x, y, 0x1);
179 			}
180 		}
181 		for (int y = 2 ; y < 6 ; y++) {
182 			for (int x ; x < 6 ; x++) {
183 				snakeSW.writePixel(x, y, 0x1);
184 			}
185 		}
186 
187 		//We now have all the necessary graphics elements, let's create the rest!
188 		//First, we need an output screen. It's 4:3, which isn't very modern, but will be useful for demo purposes.
189 		output = new OutputScreen("Snek game", 320 * 4, 240 * 4);
190 		//Next, we have to create the raster, with 320x240 resolution, and 256 colors. Technically we will only use 3 
191 		//colors, however one should overprovision the palette length to the multiple of the maximum color of the used
192 		//indexed bitmap. (16 bit is mainly there to directly access all colors of the palette)
193 		raster = new Raster(320, 240, output, 256);
194 		//Set up the color palette
195 		raster.setPaletteIndex(0, Color(0,0,0,255));
196 		raster.setPaletteIndex(1, Color(255,255,255,255));
197 		raster.setPaletteIndex(2, Color(255,0,0,255));
198 		//Let's create our playfield, where the action will happen.
199 		//The `Copy` rendering mode does not support transparency or anything fancy like that, but is very fast and
200 		//harder to make things go bad.
201 		playfield = new TileLayer(8, 8, RenderingMode.Copy);
202 		//Add the tile layer to the raster.
203 		raster.addLayer(playfield, 0);
204 		//Load an empty mapping into our playfield, which we will be modifying later through the `readMapping` and 
205 		//`writeMapping` functions.
206 		{
207 			MappingElement[] emptyMap;
208 			emptyMap.length = 40 * 30;
209 			playfield.loadMapping(40, 30, emptyMap);
210 		}
211 		//Add the tiles to the playfield
212 		playfield.addTile(empty, TileTypes.Empty);
213 		playfield.addTile(apple, TileTypes.Apple);
214 		playfield.addTile(snakeH, TileTypes.SnakeH);
215 		playfield.addTile(snakeV, TileTypes.SnakeV);
216 		playfield.addTile(snakeNE, TileTypes.SnakeNE);
217 		playfield.addTile(snakeSE, TileTypes.SnakeSE);
218 		playfield.addTile(snakeNW, TileTypes.SnakeNW);
219 		playfield.addTile(snakeSW, TileTypes.SnakeSW);
220 		//Initialize input handler
221 		ih = new InputHandler();
222 		ih.systemEventListener = this;
223 		ih.inputListener = this;
224 		//Register key bindings.
225 		ih.addBinding(BindingCode(ScanCode.UP, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("up"));
226 		ih.addBinding(BindingCode(ScanCode.DOWN, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("down"));
227 		ih.addBinding(BindingCode(ScanCode.LEFT, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("left"));
228 		ih.addBinding(BindingCode(ScanCode.RIGHT, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("right"));
229 		ih.addBinding(BindingCode(ScanCode.ENTER, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("start"));
230 		ih.addBinding(BindingCode(ScanCode.ESCAPE, 0, Devicetype.Keyboard, 0, KeyModifier.All), InputBinding("quit"));
231 		//Register an initial timer event
232 		timer.register(&timerEvent, msecs(200));
233 	}
234 	/// Main loop cycle, where all the good things happen.
235 	public void whereTheMagicHappens() {
236 		while (state != 4) {
237 			if (state == 5)
238 				timer.test();
239 			ih.test();
240 			raster.refresh();
241 			rng.seed();
242 		}
243 	}
244 	/// Places the next apple to a random empty location.
245 	/// If it can no longer put any new apples anywhere, then the game have been won.
246 	public void placeNextApple() {
247 		const int x = cast(int)(rng() % 40), y = cast(int)(rng() & 30);
248 		for (int yi ; yi < 30 ; yi++) {
249 			for (int xi ; xi < 40 ; xi++) {
250 				if (playfield.readMapping((xi + x) % 40, (yi + y) % 30).tileID == TileTypes.Empty) {
251 					playfield.writeMapping((xi + x) % 40, (yi + y) % 30, MappingElement(TileTypes.Apple));
252 					return;
253 				}
254 			}
255 		}
256 		//If didn't escaped from the loop, then it means we can place no more apples, and the player have won.
257 		state = 7;
258 	}
259 	///Moves the snake in the given direction.
260 	public void moveSnake() {
261 		prevDir = dir;
262 		Point curr, prev = snakeHead;
263 		bool appleFound;
264 		switch (dir) {
265 			default:
266 				return;
267 			case Direction.North:
268 				snakeHead.relMove(0, -1);
269 				break;
270 			case Direction.South:
271 				snakeHead.relMove(0, 1);
272 				break;
273 			case Direction.East:
274 				snakeHead.relMove(1, 0);
275 				break;
276 			case Direction.West:
277 				snakeHead.relMove(-1, 0);
278 				break;
279 		}
280 		curr = snakeHead;
281 		// Check for collision
282 		MappingElement me = playfield.readMapping(curr.x, curr.y);
283 		if (me.tileID == TileTypes.Apple) {	//Player has collected an apple, let's reward them!
284 			score++;
285 			placeNextApple();
286 			appleFound = true;
287 		} else if (me.tileID != TileTypes.Empty || !Box(0, 0, 39, 29).isBetween(curr)) {	//Player has died, stop game.
288 			raster.setPaletteIndex(0, Color(255,0,0,255));
289 			state = 6;
290 			return;
291 		}
292 		// Draw the head to the new place
293 		if (dir == Direction.East || dir == Direction.West)
294 			playfield.writeMapping(curr.x, curr.y, MappingElement(TileTypes.SnakeH));
295 		else
296 			playfield.writeMapping(curr.x, curr.y, MappingElement(TileTypes.SnakeV));
297 		// Move the snake.
298 		// This iteration finds the endpiece of the snake, by using the shape identifier codes.
299 		for (int i ; i <= score ; i++) {
300 			//MappingElement currT = playfield.readMapping(curr.x, curr.y);
301 			MappingElement prevT = playfield.readMapping(prev.x, prev.y);
302 			switch (prevT.tileID) {
303 				case TileTypes.SnakeH:
304 					const int cX = curr.x, pX = prev.x;
305 					curr.x = prev.x;
306 					if (pX < cX)
307 						prev.x -= 1;
308 					else
309 						prev.x += 1;
310 					break;
311 				case TileTypes.SnakeV:
312 					const int cY = curr.y, pY = prev.y;
313 					curr.y = prev.y;
314 					if (pY < cY)
315 						prev.y -= 1;
316 					else
317 						prev.y += 1;
318 					break;
319 				case TileTypes.SnakeNE:
320 					const int cX = curr.x, pX = prev.x;
321 					curr = prev;
322 					if (cX == pX)
323 						prev.x += 1;
324 					else
325 						prev.y -= 1;
326 					break;
327 				case TileTypes.SnakeSE:
328 					const int cX = curr.x, pX = prev.x;
329 					curr = prev;
330 					if (cX == pX)
331 						prev.x += 1;
332 					else
333 						prev.y += 1;
334 					break;
335 				case TileTypes.SnakeNW:
336 					const int cX = curr.x, pX = prev.x;
337 					curr = prev;
338 					if (cX == pX)
339 						prev.x -= 1;
340 					else
341 						prev.y -= 1;
342 					break;
343 				case TileTypes.SnakeSW:
344 					const int cX = curr.x, pX = prev.x;
345 					curr = prev;
346 					if (cX == pX)
347 						prev.x -= 1;
348 					else
349 						prev.y += 1;
350 					break;
351 				default:
352 					break;	//Not nice I know, but there's no better way currently to break from multiple levels
353 			}
354 		}
355 		
356 		// Remove end if growth doesn't happen.
357 		if (!appleFound) 
358 			playfield.writeMapping(curr.x, curr.y, MappingElement(TileTypes.Empty));
359 	}
360 	/// Since tilemaps get initialized with 0xFFFF (none) tiles, we need to set it to 0x0000
361 	public void clearTilemap() {
362 		for (int y ; y < 30 ; y++) {
363 			for (int x ; x < 40 ; x++) {
364 				playfield.writeMapping(x,y, MappingElement(TileTypes.Empty));
365 			}
366 		}
367 	}
368 	/// A timer event, to make the game run stable regardless of the framerate.
369 	public void timerEvent(Duration jitter) {
370 		//Reregister timer event
371 		timer.register(&timerEvent, msecs(200));
372 		//Move snake if needed
373 		if (dir) {
374 			moveSnake();
375 		}
376 	}
377 	/// Key event data is received here.
378 	public void keyEvent(uint id, BindingCode code, uint timestamp, bool isPressed) {
379 		switch (id) {
380 			case hashCalc("up"):
381 				if (prevDir != Direction.South) {
382 					dir = Direction.North;
383 				}
384 				switch (prevDir) {
385 					case Direction.East:
386 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeNW));
387 						break;
388 					case Direction.West:
389 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeNE));
390 						break;
391 					default: break;
392 				}
393 				break;
394 			case hashCalc("down"):
395 				if (prevDir != Direction.North) {
396 					dir = Direction.South;
397 				}
398 				switch (prevDir) {
399 					case Direction.East:
400 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeSW));
401 						break;
402 					case Direction.West:
403 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeSE));
404 						break;
405 					default: break;
406 				}
407 				break;
408 			case hashCalc("left"):
409 				if (prevDir != Direction.East) {
410 					dir = Direction.West;
411 				}
412 				switch (prevDir) {
413 					case Direction.North:
414 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeSW));
415 						break;
416 					case Direction.South:
417 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeNW));
418 						break;
419 					default: break;
420 				}
421 				break;
422 			case hashCalc("right"):
423 				if (prevDir != Direction.West) {
424 					dir = Direction.East;
425 				}
426 				switch (prevDir) {
427 					case Direction.North:
428 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeSE));
429 						break;
430 					case Direction.South:
431 						playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeNE));
432 						break;
433 					default: break;
434 				}
435 				break;
436 			case hashCalc("start"):
437 				raster.setPaletteIndex(0, Color(0,0,0,255));
438 				clearTilemap();
439 				state = 5;
440 				snakeHead = Point(20, 15);
441 				score = 0;
442 				dir = 0;
443 				prevDir = 0;
444 				placeNextApple();
445 				playfield.writeMapping(snakeHead.x, snakeHead.y, MappingElement(TileTypes.SnakeH));
446 				break;
447 			case hashCalc("quit"):
448 				state = 4;
449 				break;
450 			default: break;
451 		}
452 	}
453 
454 	public void axisEvent(uint id, BindingCode code, uint timestamp, float value) {
455 		
456 	}
457 	/// Makes it possible to close the game the `proper` way.
458 	public void onQuit() {
459 		state = 4;
460 	}
461 
462 	public void controllerAdded(uint id) {
463 		
464 	}
465 
466 	public void controllerRemoved(uint id) {
467 		
468 	}
469 }