diff --git a/CONTRACT_Platform-Game.md b/CONTRACT_Platform-Game.md new file mode 100644 index 0000000..70a1caf --- /dev/null +++ b/CONTRACT_Platform-Game.md @@ -0,0 +1,72 @@ +#bitter-steenbok +## Somaya Bounouar +#363 Browser Games: Generic Platform Game +http://jsdev.learnersguild.org/goals/363-Browser_Games-Platform.html +Specs are in the CONTRACT file. + +# Browser Games: Generic Platform Game + +## Challenge Rating + +This goal will likely be within your ZPD if you... + +- Can build basic web sites with HTML & CSS +- Can add behavior to a web site with JavaScript +- Are familiar with DOM manipulation +- Are familiar with platform-based games +- Are interested in making more complex interactive web pages + +## Description + +Implement a simple [platform](https://en.wikipedia.org/wiki/Platform_game) ("run and jump") game with HTML, CSS, and JavaScript. + +Follow [this tutorial](http://eloquentjavascript.net/15_game.html) from [Eloquent JavaScript](http://eloquentjavascript.net/). + +Fork the the [browser-games repository][browser-games] and use the fork as your project artifact. + +Implement the **Platform** game from the list in the [games.md][games-list] file. + + + +## Context + +This goal will challenge your ability to take a _formal, defined system_ from the real world and replicate it in code. You will start with all of the logic of the system (the rules of the game) and most of the UI already designed. + +Your work will be mainly in deciding how to replicate that formal logic and user interface using only JavaScript, HTML, and CSS. + +## Specifications + +#### General + +- [x] Artifact produced is a fork of the [browser-games][browser-games] repo. +- [x] Variables, functions, files, etc. have appropriate and meaningful names. +- [x] HTML, CSS, and JS files are well formatted with proper spacing and indentation. +- [x] There is a clear separation of game logic code from view/rendering code. +- [x] All major features are added via pull requests with a clear description and concise commit messages. +- [x] The artifact produced is properly licensed, preferably with the [MIT license][mit-license]. + +#### Generic Platform Game + +- [x] Game can be found at `public/platform.html` +- [x] Game is playable by one player +- [x] Game follows rules established in [tutorial](http://eloquentjavascript.net/15_game.html) +- [x] Game page is linked from `public/index.html` + +### Stretch + +Design and build your own platform-like game. What else can you build with the techniques you came up with in building the Generic Platform Game? + +- [ ] Game has its own HTML, CSS, and JS +- [ ] Game is playable +- [ ] Game page is linked from `public/index.html` + +## Resources + +- MDN: [Introduction to the DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction) #html #dom #js +- MDN: [Guide to Event Handlers](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers) #dom #js +- Shay Howe: [Learn to Code HTML & CSS](http://learn.shayhowe.com/html-css/) #html #css +- Tutorial: [Project: A Platform Game](http://eloquentjavascript.net/15_game.html) #js #html #dom + +[browser-games]: https://github.com/GuildCrafts/browser-games +[games-list]: https://github.com/GuildCrafts/browser-games/blob/master/games.md +[mit-license]: https://opensource.org/licenses/MIT diff --git a/README.md b/README.md index 09a972f..6a1d704 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +#bitter-steenbok +## Somaya Bounouar +#363 Browser Games: Generic Platform Game +http://jsdev.learnersguild.org/goals/363-Browser_Games-Platform.html + # Browser Games A collection of games to play in a web browser. See the full list of games in the [games.md](games.md) file. diff --git a/public/css/platform.css b/public/css/platform.css new file mode 100644 index 0000000..bd69d76 --- /dev/null +++ b/public/css/platform.css @@ -0,0 +1,43 @@ +.background { + background: rgb(52, 166, 251); + table-layout: fixed; + border-spacing: 0; +} +.background td { + padding: 0; +} +.lava { + background: rgb(255, 100, 100); +} +.wall { + background: white; +} + +.actor { position: absolute; } +.coin { background: rgb(241, 229, 89); } +.player { background: rgb(64, 64, 64); } + +.lost .player { + background: rgb(160, 64, 64); +} +.won .player { + box-shadow: -4px -7px 8px white, 4px -7px 8px white; +} + +.game { + overflow: hidden; + max-width: 600px; + max-height: 450px; + position: relative; +} + +#status { + font-family: futura, arial; + font-weight: bold; + color: #152055; +} + +#instructions { + font-family: futura, arial; + color: #152055; +} diff --git a/public/index.html b/public/index.html index 278d291..a2a6388 100644 --- a/public/index.html +++ b/public/index.html @@ -13,9 +13,7 @@
+ Instructions: Collect all the coins to win. Use the arrows to move through the game. Press the ESC key to pause the game. +
+diff --git a/public/js/GAME_LEVELS.js b/public/js/GAME_LEVELS.js new file mode 100644 index 0000000..a82bf33 --- /dev/null +++ b/public/js/GAME_LEVELS.js @@ -0,0 +1,137 @@ +var GAME_LEVELS = [ + [" ", + " ", + " ", + " ", + " ", + " ", + " xxx ", + " xx xx xx!xx ", + " o o xx x!!!x ", + " xx!xx ", + " xxxxx xvx ", + " xx ", + " xx o o x ", + " x o x ", + " x xxxxx o x ", + " x xxxx o x ", + " x @ x x xxxxx x ", + " xxxxxxxxxxxx xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxxx ", + " x x x x ", + " x!!!x x!!!!!x ", + " x!!!x x!!!!!x ", + " xxxxx xxxxxxx ", + " ", + " "], + [" x!!x xxxxxxx x!x ", + " x!!x xxxx xxxx x!x ", + " x!!xxxxxxxxxx xx xx x!x ", + " xx!!!!!!!!!!xx xx xx x!x ", + " xxxxxxxxxx!!x x o o o x!x ", + " xx!x x o o xx!x ", + " x!x x xxxxxxxxxxxxxxx!!x ", + " xvx x x x !!!!!!!!!!!!!!xx ", + " xx | | | xx xxxxxxxxxxxxxxxxxxxxx ", + " xx!!!!!!!!!!!xx v ", + " xxxx!!!!!xxxx ", + " x x xxxxxxx xxx xxx ", + " x x x x x x ", + " x x x x ", + " x x xx x ", + " xx x x x ", + " x x o o x x x x ", + " xxxxxxx xxx xxx x x x x x x ", + " xx xx x x x x xxxxxx x x xxxxxxxxx x ", + " xx xx x o x x xx x x x x ", + " @ x x x x x x x x x x ", + " xxx x x x x x x x xxxxx xxxxxx x ", + " x x x x xx o xx x x x o x x x ", + "!!!!x x!!!!!!x x!!!!!!xx xx!!!!!!!!xx x!!!!!!!!!! x = x x x ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxx x!!!!!!!xx! xxxxxxxxxxxxx xx o o xx ", + "!!!!x x!!!!!!x x!!!!!x o xx!!!!!!xx ! xx xx ", + "!!!!x x!!!!!!x x!!!!!x xx!!!!!!xx ! xxxxxxx ", + "!!!!x x!!!!!!x x!!!!!xx xxxxxxxxxxxxxx!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!xxxxxxxxx!!!!!!!!!!!!!!!!!!xx ! ", + "!!!!x x!!!!!!x x!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!xx ! "], + [" ", + " ", + " ", + " ", + " ", + " o ", + " ", + " x ", + " x ", + " x ", + " x ", + " xxx ", + " x x !!! !!! xxx ", + " x x !x! !x! ", + " xxx xxx x x ", + " x x x oooo x xxx ", + " x x x x x!!!x ", + " x x xxxxxxxxxxxx xxx ", + " xx xx x x x ", + " x xxxxxxxxx xxxxxxxx x x ", + " x x x x!!!x ", + " x x x xxx ", + " xx xx x ", + " x x= = = = x xxx ", + " x x x x!!!x ", + " x x = = = =x o xxx xxx ", + " xx xx x x!!!x ", + " o o x x x x xxv xxx ", + " x x x x x!!!x ", + " xxx xxx xxx xxx o o x!!!!!!!!!!!!!!x vx ", + " x xxx x x xxx x x!!!!!!!!!!!!!!x ", + " x x xxxxxxxxxxxxxxxxxxxxxxx ", + " xx xx xxx ", + " xxx x x x x!!!x xxx ", + " x x x xxx x xxx x x ", + " x x xxx xxxxxxx xxxxx x ", + " x x x x x x ", + " x xx x x x x x ", + " x x |xxxx| |xxxx| xxx xxx x ", + " x xxx o o x x xxx x ", + " x xxxxx xx x xxx x!!!x x x ", + " x oxxxo x xxx x x x xxx xxx x ", + " x xxx xxxxxxxxxxxxx x oo x x oo x x oo xx xx xxx x ", + " x @ x x x!!x x!!!!x x!!!!x xx xx x x ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ", + " ", + " "], + [" xxx x ", + " x ", + " xxxxx ", + " x ", + " x xxx ", + " o x x x ", + " o o oxxx x ", + " xxx x ", + " ! o ! xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxx ", + " x x x x x x x x x x x x x x x ", + " x= o x x xxx x xxx x xxx x xxx x xxx x xxx x xxxxx ", + " x x x x x x x x x x x x x x x ", + " ! o ! o xxxx xxxxx xxxxx xxxxx xxxxx xxxxx xxxxxxx ", + " ", + " o xxx xx ", + " ", + " ", + " xx ", + " xxx xxx ", + " ", + " o x x ", + " xx xx ", + " xxx xxx xxx x x ", + " ", + " || ", + " xxxxxxxxxxx ", + " x x o xxxxxxxxx o xxxxxxxxx o xx x ", + " x x x x x x x || x x ", + " x @ xxxxx o xxxxx o xxxxx ", + " xxxxxxx xxxxx xx xx xxx ", + " x= = =x x xxx ", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x!!!!!!!!!!!!!!!!!!!!!xxx!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", + " xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + " "] +]; diff --git a/public/js/platform.js b/public/js/platform.js new file mode 100644 index 0000000..6cc92f1 --- /dev/null +++ b/public/js/platform.js @@ -0,0 +1,387 @@ + +function Level(plan) { + this.width = plan[0].length; + this.height = plan.length; + this.grid = []; + this.actors = []; + + for (var y = 0; y < this.height; y++) { + var line = plan[y] + var gridLine = []; + for (var x = 0; x < this.width; x++) { + var ch = line[x] + var fieldType = null; + var Actor = actorChars[ch]; + if (Actor) + this.actors.push(new Actor(new Vector(x, y), ch)); + else if (ch == "x") + fieldType = "wall"; + else if (ch == "!") + fieldType = "lava"; + gridLine.push(fieldType); + } + this.grid.push(gridLine); + } + this.player = this.actors.filter(function(actor) { + return actor.type == "player"; + })[0]; + this.status = this.finishDelay = null; +} +Level.prototype.isFinished = function() { + return this.status != null && this.finishDelay < 0; +} + +function Vector(x, y) { + this.x = x; + this.y = y; +} +Vector.prototype.plus = function(other) { + return new Vector(this.x + other.x, this.y + other.y); +} +Vector.prototype.times = function(factor) { + return new Vector(this.x * factor, this.y * factor); +} + +var actorChars = { + "@": Player, + "o": Coin, + "=": Lava, "|": Lava, "v": Lava +} + +function Player(pos) { + this.pos = pos.plus(new Vector(0, -0.5)); + this.size = new Vector(0.8, 1.5); + this.speed = new Vector(0, 0); +} +Player.prototype.type = "player"; + +function Lava(pos, ch) { + this.pos = pos; + this.size = new Vector(1, 1); + if (ch == "=") { + this.speed = new Vector(2, 0); + } else if (ch == "|") { + this.speed = new Vector(0, 2); + } else if (ch == "v") { + this.speed = new Vector(0, 3); + this.repeatPos = pos; + } +} +Lava.prototype.type = "lava"; + +function Coin(pos) { + this.basePos = this.pos = pos.plus(new Vector(0.2, 0.1)); + this.size = new Vector(0.6, 0.6); + this.wobble = Math.random() * Math.PI * 2; +} +Coin.prototype.type = "coin"; + +function elt(name, className) { + var elt = document.createElement(name); + if (className) { + elt.className = className; + } + return elt; +} + +function DOMDisplay(parent, level) { + this.wrap = parent.appendChild(elt("div", "game")); + this.level = level; + + this.wrap.appendChild(this.drawBackground()); + this.actorLayer = null; + this.drawFrame(); +} + +var scale = 20; + +DOMDisplay.prototype.drawBackground = function() { + var table = elt("table", "background"); + table.style.width = this.level.width * scale + "px"; + this.level.grid.forEach(function(row) { + var rowElt = table.appendChild(elt("tr")); + rowElt.style.height = scale + "px"; + row.forEach(function(type) { + rowElt.appendChild(elt("td", type)); + }) + }) + return table; +} + +DOMDisplay.prototype.drawActors = function() { + var wrap = elt("div"); + this.level.actors.forEach(function(actor) { + var rect = wrap.appendChild(elt("div", "actor " + actor.type)); + rect.style.width = actor.size.x * scale + "px"; + rect.style.height = actor.size.y * scale + "px"; + rect.style.left = actor.pos.x * scale + "px"; + rect.style.top = actor.pos.y * scale + "px"; + }) + return wrap; +} + +DOMDisplay.prototype.drawFrame = function() { + if (this.actorLayer) { + this.wrap.removeChild(this.actorLayer); + } + this.actorLayer = this.wrap.appendChild(this.drawActors()); + this.wrap.className = "game " + (this.level.status || ""); + this.scrollPlayerIntoView(); +} + +DOMDisplay.prototype.scrollPlayerIntoView = function() { + var width = this.wrap.clientWidth; + var height = this.wrap.clientHeight; + var margin = width / 3; + + var left = this.wrap.scrollLeft, right = left + width; + var top = this.wrap.scrollTop, bottom = top + height; + + var player = this.level.player; + var center = player.pos.plus(player.size.times(0.5)).times(scale); + if (center.x < left + margin) + this.wrap.scrollLeft = center.x - margin; + else if (center.x > right - margin) + this.wrap.scrollLeft = center.x + margin - width; + if (center.y < top + margin) + this.wrap.scrollTop = center.y - margin; + else if (center.y > bottom - margin) + this.wrap.scrollTop = center.y + margin - height; +} + +DOMDisplay.prototype.clear = function() { + this.wrap.parentNode.removeChild(this.wrap); +} + +Level.prototype.obstacleAt = function(pos, size) { + var xStart = Math.floor(pos.x); + var xEnd = Math.ceil(pos.x + size.x); + var yStart = Math.floor(pos.y); + var yEnd = Math.ceil(pos.y + size.y); + + if (xStart < 0 || xEnd > this.width || yStart < 0) + return "wall"; + if (yEnd > this.height) + return "lava"; + for (var y = yStart; y < yEnd; y++) { + for (var x = xStart; x < xEnd; x++) { + var fieldType = this.grid[y][x]; + if (fieldType) return fieldType; + } + } +} + +Level.prototype.actorAt = function(actor) { + for (var i = 0; i < this.actors.length; i++) { + var other = this.actors[i]; + if (other != actor && + actor.pos.x + actor.size.x > other.pos.x && + actor.pos.x < other.pos.x + other.size.x && + actor.pos.y + actor.size.y > other.pos.y && + actor.pos.y < other.pos.y + other.size.y) + return other; + } +} + +var maxStep = 0.05; + +Level.prototype.animate = function(step, keys) { + if (this.status != null) + this.finishDelay -= step; + + while (step > 0) { + var thisStep = Math.min(step, maxStep); + this.actors.forEach(function(actor) { + actor.act(thisStep, this, keys); + }, this); + step -= thisStep; + } +} + +Lava.prototype.act = function(step, level) { + var newPos = this.pos.plus(this.speed.times(step)); + if (!level.obstacleAt(newPos, this.size)) + this.pos = newPos; + else if (this.repeatPos) + this.pos = this.repeatPos; + else + this.speed = this.speed.times(-1); +} + +var wobbleSpeed = 8, wobbleDist = 0.07; + +Coin.prototype.act = function(step) { + this.wobble += step * wobbleSpeed; + var wobblePos = Math.sin(this.wobble) * wobbleDist; + this.pos = this.basePos.plus(new Vector(0, wobblePos)); +} + +var playerXSpeed = 7; + +Player.prototype.moveX = function(step, level, keys) { + this.speed.x = 0; + if (keys.left) this.speed.x -= playerXSpeed; + if (keys.right) this.speed.x += playerXSpeed; + + var motion = new Vector(this.speed.x * step, 0); + var newPos = this.pos.plus(motion); + var obstacle = level.obstacleAt(newPos, this.size); + if (obstacle) + level.playerTouched(obstacle); + else + this.pos = newPos; +} + +var gravity = 30; +var jumpSpeed = 17; + +Player.prototype.moveY = function(step, level, keys) { + this.speed.y += step * gravity; + var motion = new Vector(0, this.speed.y * step); + var newPos = this.pos.plus(motion); + var obstacle = level.obstacleAt(newPos, this.size); + if (obstacle) { + level.playerTouched(obstacle); + if (keys.up && this.speed.y > 0) + this.speed.y = -jumpSpeed; + else + this.speed.y = 0; + } else { + this.pos = newPos; + } +} + +Player.prototype.act = function(step, level, keys) { + this.moveX(step, level, keys); + this.moveY(step, level, keys); + + var otherActor = level.actorAt(this); + if (otherActor) + level.playerTouched(otherActor.type, otherActor); + + if (level.status == "lost") { + this.pos.y += step; + this.size.y -= step; + } +} + +Level.prototype.playerTouched = function(type, actor) { + if (type == "lava" && this.status == null) { + this.status = "lost"; + this.finishDelay = 1; + } else if (type == "coin") { + this.actors = this.actors.filter(function(other) { + return other != actor; + }); + if (!this.actors.some(function(actor) { + return actor.type == "coin"; + })) { + this.status = "won"; + this.finishDelay = 1; + } + } +} + +var arrowCodes = {37: "left", 38: "up", 39: "right"}; + +function trackKeys(codes) { + var pressed = Object.create(null); + function handler(event) { + if (codes.hasOwnProperty(event.keyCode)) { + var down = event.type == "keydown"; + pressed[codes[event.keyCode]] = down; + event.preventDefault(); + } + } + addEventListener("keydown", handler); + addEventListener("keyup", handler); + return pressed; +} + +function runAnimation(frameFunc) { + var lastTime = null; + function frame(time) { + var stop = false; + if (lastTime != null) { + var timeStep = Math.min(time - lastTime, 100) / 1000; + stop = frameFunc(timeStep) === false; + } + lastTime = time; + if (!stop) + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); +} + +var arrows = trackKeys(arrowCodes); + +function runLevel(level, Display, andThen) { + var display = new Display(document.body, level); + + var running = 'yes'; + + function handleKey(event) { + if(event.keyCode == 27) { + if (running == 'no') { + running = 'yes'; + runAnimation(animation); + } + else if (running == 'pausing') { + running = 'yes'; + } + else if (running == 'yes') { + running = 'pausing' + } + } + } + addEventListener('keydown', handleKey); + + function animation(step) { + if(running == 'pausing') { + running = 'no'; + return false; + } + + level.animate(step, arrows); + display.drawFrame(step); + + if(level.isFinished()) { + display.clear(); + + if(andThen) { + andThen(level.status); + return false + } + } + } + runAnimation(animation); +} + +function runGame(plans, Display) { + var lives = 3; + var livesSpan = document.getElementById('lives'); + var gameStatus = document.getElementById('status'); + + function startLevel(n) { + livesSpan.textContent = lives; + runLevel(new Level(plans[n]), Display, function(status) { + if (status == 'lost') { + lives--; + if (lives == 0) { + gameStatus.textContent = 'Game Over'; + console.log('Game over'); + } else { + startLevel(n); + } + } + else if (n < plans.length - 1) { + startLevel(n + 1); + } else { + console.log("You win!"); + } + }) + } + startLevel(0); +} + +runGame(GAME_LEVELS, DOMDisplay) diff --git a/public/platform.html b/public/platform.html new file mode 100644 index 0000000..d065ec3 --- /dev/null +++ b/public/platform.html @@ -0,0 +1,21 @@ + + +
+ +
+ + +
+