<!DOCTYPE html>
<html>
<head>
<title>angular2 playground</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css" />
<script src="https://code.angularjs.org/tools/system.js"></script>
<script src="https://code.angularjs.org/tools/typescript.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.5/immutable.min.js"></script>
<script src="config.js"></script>
<script src="https://code.angularjs.org/2.0.0-alpha.46/angular2.min.js"></script>
<script>
System.import('app')
.catch(console.error.bind(console));
</script>
</head>
<body>
<app>
Loading, please wait...
</app>
</body>
</html>
/* Styles go here */
body {
background: #b5afa7 no-repeat 23px 30px;
background-size: 380px;
font-family: sans-serif;
}
#main {
margin: 0 auto 0;
width: 800px;
}
.time {
background: #736d65;
display: inline-block;
padding: 6px 10px;
}
.board {
border: 10px solid #a49e96;
border-radius: 10px;
margin: 20px;
background: #938d85;
display: inline-block;
width: 512px;
}
.tile {
position: relative;
border: 1px solid #504d49;
width: 30px;
height: 30px;
float: left;
color: #100d09;
font-weight: bold;
line-height: 30px;
font-size: 20px;
vertical-align: middle;
text-align: center;
}
.lid {
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border: 2px outset #a49e96;
background-image:
radial-gradient(
circle at top left,
#a49e96,
#605d59
);
}
.lid:hover {
background-image:
radial-gradient(
circle at bottom right,
#a49e96,
#605d59
);
}
.mine {
background: url(../images/danger.png);
background: red;
}
a:hover{
cursor: pointer;
}
ul.actions{
list-style: none;
margin: 0 20px;
padding: 0;
}
ul.actions li{
display: inline-block;
}
ul.actions a{
border: 10px solid #a49e96;
border-radius: 10px;
padding: 6px 12px;
text-decoration: none;
font-weight: bold;
color: rgba(0,0,0,0.5);
}
ul.actions a:hover{
background: #a49e96;
}
System.config({
//use typescript for compilation
transpiler: 'typescript',
//typescript compiler options
typescriptOptions: {
emitDecoratorMetadata: true
},
//map tells the System loader where to look for things
map: {
app: "./src"
},
//packages defines our app package
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
}
}
});
// Import Angular dependencies
import {bootstrap, provide} from 'angular2/angular2';
// Import application component
import {App} from './app/app.component';
// Bootstrap the application
bootstrap(App, []).catch(err => console.error(err));
import {Component, Input, CORE_DIRECTIVES, ChangeDetectionStrategy} from 'angular2/angular2';
import {revealTile, isGameOver} from './game';
import {TileComponent} from './tile.component';
@Component({
selector: 'minesweeper',
template: `
<div class="board">
<tile *ng-for="#tile of getTiles()" [tile]="tile" (tile-click)="handleTileClick($event)"></tile>
</div>
`,
directives: [CORE_DIRECTIVES, TileComponent]
// If we enable this, the undo action does not update UI
// changeDetection: ChangeDetectionStrategy.OnPush
})
export class MinesweeperComponent {
@Input() game: any;
history = Immutable.List();
onChanges(changes){
// Only update game when game has actually changed
if(changes.hasOwnProperty('game')){
this.updateGame()
}
}
getTiles(){
return this.game ? this.game.get('tiles') : [];
}
updateGame(updateHistory = true){
this.history = this.history.push(this.game);
}
handleTileClick(tile){
if(!tile){
return;
}
if (isGameOver(this.game)) {
return;
}
const newGame = revealTile(this.game, tile.get('id'));
if (newGame !== this.game) {
this.game = newGame;
this.updateGame();
}
if (isGameOver(this.game)) {
window.alert('GAME OVER!');
}
}
undo(){
if (this.canUndo()) {
console.log('undo');
this.history = this.history.pop();
this.game = this.history.last();
}
}
canUndo(){
return this.history.size > 1;
}
}
// Credits to Christian Johansen for game logic:
// https://github.com/cjohansen/react-sweeper
let {List,Map,fromJS} = Immutable;
import {partition, shuffle, repeat, keep, prop} from './util';
function initTiles(rows, cols, mines) {
return shuffle(repeat(mines, Map({isMine: true, isRevealed: false})).
concat(repeat(rows * cols - mines, Map({isRevealed: false})))).
map(function (tile, idx) {
return tile.set('id', idx);
});
}
function onWEdge(game, tile) {
return tile % game.get('cols') === 0;
}
function onEEdge(game, tile) {
return tile % game.get('cols') === game.get('cols') - 1;
}
function idx(game, tile) {
if (tile < 0) { return null; }
return game.getIn(['tiles', tile]) ? tile : null;
}
function nw(game, tile) {
return onWEdge(game, tile) ? null : idx(game, tile - game.get('cols') - 1);
}
function n(game, tile) {
return idx(game, tile - game.get('cols'));
}
function ne(game, tile) {
return onEEdge(game, tile) ? null : idx(game, tile - game.get('cols') + 1);
}
function e(game, tile) {
return onEEdge(game, tile) ? null : idx(game, tile + 1);
}
function se(game, tile) {
return onEEdge(game, tile) ? null : idx(game, tile + game.get('cols') + 1);
}
function s(game, tile) {
return idx(game, tile + game.get('cols'));
}
function sw(game, tile) {
return onWEdge(game, tile) ? null : idx(game, tile + game.get('cols') - 1);
}
function w(game, tile) {
return onWEdge(game, tile) ? null : idx(game, tile - 1);
}
const directions = [nw, n, ne, e, se, s, sw, w];
function neighbours(game, tile) {
return keep(directions, function (dir) {
return game.getIn(['tiles', dir(game, tile)]);
});
}
function getMineCount(game, tile) {
var nbs = neighbours(game, tile);
return nbs.filter(prop('isMine')).length;
}
function isMine(game, tile) {
return game.getIn(['tiles', tile, 'isMine']);
}
function isSafe(game) {
const tiles = game.get('tiles');
const mines = tiles.filter(prop('isMine'));
return mines.filter(prop('isRevealed')) === 0 &&
tiles.length - mines.length === tiles.filter(prop('isRevealed')).length;
}
export function isGameOver(game) {
return isSafe(game) || game.get('isDead');
}
function addThreatCount(game, tile) {
return game.setIn(['tiles', tile, 'threatCount'], getMineCount(game, tile));
}
function revealAdjacentSafeTiles(game, tile) {
if (isMine(game, tile)) {
return game;
}
game = addThreatCount(game, tile).setIn(['tiles', tile, 'isRevealed'], true);
if (game.getIn(['tiles', tile, 'threatCount']) === 0) {
return keep(directions, function (dir) {
return dir(game, tile);
}).reduce(function (game, pos) {
return !game.getIn(['tiles', pos, 'isRevealed']) ?
revealAdjacentSafeTiles(game, pos) : game;
}, game);
}
return game;
}
function attemptWinning(game) {
return isSafe(game) ? game.set('isSafe', true) : game;
}
function revealMine(tile) {
return tile.get('isMine') ? tile.set('isRevealed', true) : tile;
}
function revealMines(game) {
return game.updateIn(['tiles'], function (tiles) {
return tiles.map(revealMine);
});
}
export function revealTile(game, tile) {
const updated = !game.getIn(['tiles', tile]) ?
game : game.setIn(['tiles', tile, 'isRevealed'], true);
return isMine(updated, tile) ?
revealMines(updated.set('isDead', true)) :
attemptWinning(revealAdjacentSafeTiles(updated, tile));
}
export function createGame(options) {
return fromJS({
cols: options.cols,
rows: options.rows,
playingTime: 0,
tiles: initTiles(options.rows, options.cols, options.mines)
});
}
// Credits to Christian Johansen for util logic:
// https://github.com/cjohansen/react-sweeper
let {fromJS, List, Map} = Immutable;
function partition(size, coll) {
var res = [];
for (var i = 0, l = coll.size || coll.length; i < l; i += size) {
res.push(coll.slice(i, i + size));
}
return fromJS(res);
}
function identity(v) {
return v;
}
function prop(n) {
return function (object) {
return object instanceof Map ? object.get(n) : object[n];
};
}
function keep(list, pred) {
return list.map(pred).filter(identity);
}
function repeat(n, val) {
const res = [];
while (n--) {
res.push(val);
}
return List(res);
}
function shuffle(list) {
return list.sort(function () { return Math.random() - 0.5; });
}
export {partition, identity, prop, keep, repeat, shuffle};
import {Component} from 'angular2/angular2';
import {MinesweeperComponent} from '../minesweeper/minesweeper.component';
import {createGame} from '../minesweeper/game';
@Component({
selector: 'app',
template: `
<minesweeper [game]="game" #minesweeper></minesweeper>
<ul class="actions">
<li><a (click)="startNewGame()">New game</a></li>
<li><a (click)="minesweeper.undo()" [hidden]="!minesweeper.canUndo()">Undo</a></li>
</ul>
`,
directives: [MinesweeperComponent]
})
export class App {
public game;
constructor(){
}
onInit(){
this.startNewGame();
}
startNewGame(){
this.game = createGame({cols: 16, rows: 16, mines: 48});
}
}
import {Component, Input, Output, EventEmitter, CORE_DIRECTIVES, ChangeDetectionStrategy} from 'angular2/angular2';
@Component({
selector: 'tile',
template: `
<div class="tile" [class.mine]="tile.get('isMine')" (click)="handleTileClick(tile)">
<div class="lid" *ng-if="!tile.get('isRevealed')"></div>
<div *ng-if="tile.get('isRevealed') && !tile.get('isMine')">
{{ tile.get('threatCount') > 0 ? tile.get('threatCount') : '' }}
</div>
</div>
`,
directives: [CORE_DIRECTIVES],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TileComponent {
@Input() tile: any;
@Output() tileClick: EventEmitter = new EventEmitter();
onChanges(changes){
if(changes.tile){
console.log('Tile %s changed', this.tile.get('id'));
}
}
handleTileClick(tile){
this.tileClick.next(tile);
}
}