Skip to content

Instantly share code, notes, and snippets.

@TheXenocide
Last active August 29, 2015 14:01
Show Gist options
  • Save TheXenocide/78ce5b2291dc2dc819fb to your computer and use it in GitHub Desktop.
Save TheXenocide/78ce5b2291dc2dc819fb to your computer and use it in GitHub Desktop.
A remake of one of the earliest games I ever ported to JavaScript. I decided to remake it this past weekend while showing the old one to my roommate. Complete re-implementation using AngularJS took about 4 hours.
<html>
<head><title>Lunar Lockout</title>
<script src="https://code.angularjs.org/1.2.9/angular.js"></script>
<script>
// note that ng-keydown is available since angular 1.1.5
var app = angular.module('lunar', []);
app.filter('range', function() {
return function(input, total) {
total = parseInt(total);
for (var i = 0; i < total; i++) {
input.push(i);
}
return input;
};
});
(function() {
var currentLevel = 0; // TODO: allow continuing from last session
// "allLevels" is an array of levels
// each level is an array of characters
// characters are simple objects with x and y properties on a 5x5 grid,
// 0,0 being the top left and 4,4 being the bottom right.
// the first character is always the red "objective" character
var allLevels = [
[{x:4,y:4},{x:4,y:0},{x:2,y:1},{x:1,y:2},{x:3,y:3}]
,[{x:1,y:4},{x:2,y:0},{x:4,y:1},{x:1,y:2},{x:3,y:3}]
,[{x:4,y:4},{x:3,y:0},{x:1,y:1},{x:3,y:3},{x:1,y:4}]
,[{x:1,y:4},{x:0,y:0},{x:4,y:0},{x:3,y:1},{x:2,y:3}]
,[{x:1,y:4},{x:3,y:0},{x:1,y:1},{x:3,y:4}]
,[{x:2,y:0},{x:0,y:0},{x:4,y:3},{x:1,y:4}]
,[{x:1,y:4},{x:2,y:0},{x:0,y:1},{x:3,y:2},{x:3,y:4}]
,[{x:3,y:3},{x:0,y:0},{x:3,y:0},{x:1,y:1},{x:0,y:4}]
,[{x:2,y:0},{x:4,y:0},{x:2,y:2},{x:1,y:3},{x:3,y:4}]
,[{x:4,y:4},{x:1,y:1},{x:4,y:2},{x:0,y:4},{x:2,y:4}]
,[{x:2,y:1},{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:2,y:2},{x:2,y:3}]
,[{x:4,y:4},{x:1,y:0},{x:4,y:1},{x:1,y:4}]
,[{x:3,y:3},{x:1,y:0},{x:3,y:1},{x:0,y:3},{x:2,y:4}]
,[{x:1,y:4},{x:2,y:0},{x:4,y:1},{x:0,y:2},{x:3,y:3},{x:0,y:4}]
,[{x:4,y:4},{x:4,y:0},{x:1,y:1},{x:4,y:2},{x:2,y:3}]
,[{x:2,y:0},{x:0,y:0},{x:1,y:2},{x:4,y:2},{x:1,y:4}]
,[{x:1,y:4},{x:2,y:0},{x:4,y:0},{x:1,y:2},{x:3,y:3}]
,[{x:2,y:0},{x:0,y:4},{x:2,y:4},{x:4,y:4}]
,[{x:4,y:4},{x:1,y:0},{x:2,y:1},{x:3,y:2},{x:0,y:3},{x:2,y:4}]
,[{x:3,y:3},{x:1,y:0},{x:4,y:1},{x:0,y:2},{x:1,y:4}]
,[{x:2,y:0},{x:2,y:1},{x:2,y:2},{x:4,y:2},{x:0,y:3},{x:3,y:4}]
,[{x:4,y:4},{x:0,y:1},{x:4,y:1},{x:3,y:3},{x:0,y:4}]
,[{x:1,y:4},{x:1,y:0},{x:0,y:1},{x:3,y:1},{x:4,y:3}]
,[{x:0,y:4},{x:3,y:1},{x:1,y:2},{x:3,y:4},{x:4,y:4}]
,[{x:1,y:4},{x:0,y:1},{x:2,y:1},{x:3,y:1},{x:4,y:4}]
,[{x:2,y:0},{x:0,y:0},{x:4,y:1},{x:0,y:2},{x:3,y:3}]
,[{x:4,y:4},{x:1,y:0},{x:4,y:0},{x:0,y:2},{x:3,y:2},{x:0,y:4}]
,[{x:4,y:4},{x:1,y:0},{x:4,y:0},{x:4,y:2},{x:0,y:3}]
,[{x:2,y:0},{x:0,y:0},{x:4,y:0},{x:0,y:4},{x:4,y:4}]
,[{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:0,y:2},{x:1,y:4},{x:3,y:4}]
,[{x:3,y:3},{x:0,y:0},{x:2,y:0},{x:4,y:1},{x:0,y:2},{x:1,y:4}]
,[{x:1,y:4},{x:1,y:0},{x:4,y:1},{x:0,y:4},{x:2,y:4}]
,[{x:4,y:4},{x:0,y:0},{x:1,y:0},{x:4,y:1},{x:0,y:3},{x:3,y:4}]
,[{x:4,y:4},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:3,y:3},{x:0,y:4}]
,[{x:2,y:0},{x:0,y:0},{x:4,y:0},{x:2,y:1},{x:0,y:4},{x:4,y:4}]
,[{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:0,y:3},{x:4,y:3},{x:2,y:4}]
,[{x:3,y:3},{x:0,y:0},{x:3,y:0},{x:4,y:0},{x:0,y:3}]
,[{x:1,y:4},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:1,y:2},{x:4,y:4}]
,[{x:4,y:4},{x:0,y:0},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:0,y:4}]
,[{x:1,y:4},{x:0,y:0},{x:2,y:0},{x:4,y:0},{x:4,y:3}]
];
var currentCharacters;
var selectedCharacter;
var setLevelImpl = function(level) {
currentLevel = level % allLevels.length;
currentCharacters = [];
// need to perform a deep clone to prevent altering source data from allLevels
var levelToClone = allLevels[currentLevel];
for (var charIdx in levelToClone) {
var origChar = levelToClone[charIdx];
currentCharacters.push({x: origChar.x, y: origChar.y});
}
selectedCharacter = 0;
}
setLevelImpl(currentLevel);
app.factory('board', function() {
return {
setLevel: function(level) {
setLevelImpl(level);
},
nextLevel: function() {
setLevelImpl(currentLevel + 1);
},
resetLevel: function() {
setLevelImpl(currentLevel);
},
getCurrentLevel: function() {
return currentLevel;
},
setSelectedCharacter: function(index) {
selectedCharacter = index;
},
getSelectedCharacter: function() {
return selectedCharacter;
},
selectNextCharacter: function() {
selectedCharacter = (selectedCharacter + 1) % currentCharacters.length;
},
// returns a true if the level is completed after the move, otherwise returns false
// allowed direction parameter values are: 'left', 'right', 'up', 'down'
moveSelectedCharacter: function(direction) {
// TODO: algorithm for movement
var selXY = currentCharacters[selectedCharacter];
var nearestXY;
// -1 indicates that no collision was found and there is no "nearest character" to
// run into. If this value is any value other than -1 it represents the index of
// the character nearest to the selected character on the plane it is moving in
var nearestCharIdx = -1;
// we only need to worry about that character closest to the selected character so
// so we'll set up an initial "nearest" character that is farther away than any real
// character could be
switch (direction) {
case 'left':
case 'up':
nearestXY = {x: -1, y: -1};
break;
case 'right':
case 'down':
nearestXY = {x: 5, y: 5};
break;
}
for (var idx in currentCharacters) {
if (idx == selectedCharacter) continue; // skip selected character, can't collide with yourself
var $chr = currentCharacters[idx];
if (direction == 'left' || direction == 'right') {
// find only characters with the same vertical position for left/right collisions
if ($chr.y == selXY.y) {
// if the character we're currently testing is nearer to the selected (moving) character
// than the previous closest, we'll mark the current character as the closest.
if (
(direction == 'left' && $chr.x > nearestXY.x && $chr.x < selXY.x) ||
(direction == 'right' && $chr.x < nearestXY.x && $chr.x > selXY.x)
) {
nearestXY = $chr;
nearestCharIdx = idx;
}
}
} else if (direction == 'up' || direction == 'down') {
// find only characters with the same horizontal position for up/down collisions
if ($chr.x == selXY.x) {
// if the character we're currently testing is nearer to the selected (moving) character
// than the previous closest, we'll mark the current character as the closest.
if (
(direction == 'up' && $chr.y > nearestXY.y && $chr.y < selXY.y) ||
(direction == 'down' && $chr.y < nearestXY.y && $chr.y > selXY.y)
) {
nearestXY = $chr;
nearestCharIdx = idx;
}
}
}
}
// if no characters are in the way
if (nearestCharIdx == -1) return false;
switch (direction) {
case 'left':
selXY.x = nearestXY.x + 1;
break;
case 'right':
selXY.x = nearestXY.x - 1;
break;
case 'up':
selXY.y = nearestXY.y + 1;
break;
case 'down':
selXY.y = nearestXY.y - 1;
break;
}
// character 0 is always the objective character; if he is located at 2,2
// he has completed the puzzle so we return true.
if (selectedCharacter == 0 && selXY.x == 2 && selXY.y == 2) return true;
// otherwise we return false
return false;
},
getCurrentCharacters: function() {
return currentCharacters;
}
};
});
})();
app.controller('lockout', ['$scope','$timeout','board', function($scope, $timeout, board) {
//$scope.characters = board.getCurrentCharacters();
//$scope.selected = board.getSelectedCharacter();
$scope.getTileType = function(x, y) {
var sel = board.getSelectedCharacter();
var chars = board.getCurrentCharacters();
for (var idx in chars) {
var $chr = chars[idx];
if ($chr.x == x && $chr.y == y) {
var ret = 'character idx' + idx;
if (idx == sel) ret += ' selected';
return ret;
}
}
return (x == 2 && y == 2) ? 'target' : 'none';
}
$scope.onTileClicked = function(x, y) {
var chars = board.getCurrentCharacters();
for (var idx in chars) {
var $chr = chars[idx];
if ($chr.x == x && $chr.y == y) {
board.setSelectedCharacter(idx);
return;
}
}
// TODO: click based movement
}
$scope.onKeyDown = function(ev) {
var lvlComplete = false;
var handled = false;
switch (ev.which) {
case 37:
lvlComplete = board.moveSelectedCharacter('left');
handled = true;
break;
case 38:
lvlComplete = board.moveSelectedCharacter('up');
handled = true;
break;
case 39:
lvlComplete = board.moveSelectedCharacter('right');
handled = true;
break;
case 40:
lvlComplete = board.moveSelectedCharacter('down');
handled = true;
break;
case 9:
board.selectNextCharacter();
handled = true;
break;
case 32: // space key
case 82: // r key
board.resetLevel();
handled = true;
break;
}
if (lvlComplete) {
$timeout(function() {
alert("Congratulations, you've cleared the level");
board.nextLevel();
}, 50);
}
if (handled) ev.preventDefault();
//$scope.pressed = ev.which;
// 37 == left
// 38 == up
// 39 == right
// 40 == down
// 82 == "r"
// 32 == " "
};
}]);
</script>
<style type="text/css">
td {
border: solid 2px black;
width: 50;
height: 50;
}
td.target {
border: double 2px red;
}
td.character {
border-radius:25px;
}
td.idx0 {
background-color: red;
}
td.idx1 {
background-color: orange;
}
td.idx2 {
background-color: green;
}
td.idx3 {
background-color: purple;
}
td.idx4 {
background-color: yellow;
}
td.idx5 {
background-color: cyan;
}
td.selected {
border: dashed 2px black;
}
</style>
</head>
<body ng-app="lunar" ng-controller="lockout" ng-keydown="onKeyDown($event)">
<table>
<tr ng-repeat="y in [] | range: 5">
<td ng-repeat="x in [] | range: 5" id="tile{{x}}-{{y}}" class="{{getTileType(x,y)}}" ng-click="onTileClicked(x,y)">
&nbsp;
</td>
</tr>
</table>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment