diff --git a/build/css/app.css b/build/css/app.css new file mode 100644 index 0000000..1ddcd24 --- /dev/null +++ b/build/css/app.css @@ -0,0 +1,28 @@ +#game, #game-list { + max-width: 600px; +} + +h1 { + text-align: center; +} + +#board-display { + width: 50%; + list-style: none; +} + +.board-square { + height: 100px; + width: 100px; + background-color: pink; + padding: 5px; + border: 1px solid white; + line-height: 100px; + text-align: center; + font-size: 4em; + color: white; +} + +.end-of-game { + display: none; +} diff --git a/build/index.html b/build/index.html index 00eb541..87f6a9c 100644 --- a/build/index.html +++ b/build/index.html @@ -3,8 +3,53 @@ Tic-Tac-Toe + + + +
+ +
+

Tic Tac Toe with Backbone

+
+ +
+
+

+
+
+ +
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+ +
+
+
+
diff --git a/spec/board.spec.js b/spec/board.spec.js new file mode 100644 index 0000000..4ac22ae --- /dev/null +++ b/spec/board.spec.js @@ -0,0 +1,58 @@ +import Board from 'app/models/board'; + +describe('Board', function(){ + describe('constructor', function(){ + it('should make a positions array of length 9', function(){ + var board1 = new Board(); + expect(board1.positions instanceof Array).toBeTruthy(); + + expect(board1.positions[0]).toEqual(" "); + + expect(board1.positions.length).toEqual(9); + }); + }); + + describe('validPlay', function(){ + var board2 = new Board(); + it('should return true if position is available', function() { + expect(board2.validPlay(1)).toBeTruthy(); + + expect(board2.validPlay(0)).toBeTruthy(); + + expect(board2.validPlay(8)).toBeTruthy(); + }); + + it('should return false if position is unavailable', function(){ + expect(board2.validPlay(2)).toBeTruthy(); + + board2.markPlay('X', 2); + + expect(board2.validPlay(2)).toBeFalsy(); + }); + + it('should throw an error if position is not an integer', function(){ + expect(function() { + board2.validPlay("cat"); + }).toThrow(new Error('Input must be an integer between 0 and 8')); + }); + + it('will return false if position is not in the positions array', function(){ + expect(board2.validPlay(9)).toBeFalsy(); + }); + }); + + describe('markPlay', function() { + var board2 = new Board(); + it('should mark the board with the play for that player', function(){ + expect(board2.markPlay('X', 1)).toEqual(1); + + expect(board2.positions[1]).toEqual('X'); + }); + + it('should throw an error if 2 arguments are not passed to it', function(){ + expect(function() { + board2.markPlay(2); + }).toThrow(new Error('Wrong number of arguments')); + }); + }); +}); diff --git a/spec/game.spec.js b/spec/game.spec.js new file mode 100644 index 0000000..4f5b1be --- /dev/null +++ b/spec/game.spec.js @@ -0,0 +1,179 @@ +import Game from 'app/models/game'; +import Board from 'app/models/board'; + +describe('Game', function(){ + describe('constructor', function() { + it('should make a new game', function(){ + var game1 = new Game(); + expect(game1 instanceof Game).toBeTruthy(); + + expect(game1.get("player1")).toEqual("X"); + + expect(game1.get("player2")).toEqual("O"); + + expect(game1.board instanceof Board).toBeTruthy(); + + expect(game1.turn).toEqual(game1.get("player1")); + }); + }); + + describe('toggleTurn', function() { + var game2 = new Game(); + it('should switch to player2 after player1', function() { + expect(game2.toggleTurn()).toEqual(game2.get("player2")); + }); + }); + + describe('gameOver', function(){ + var game3 = new Game(); + game3.board.positions = ["O","X","X","X","X","O","O","O","X"]; + it('should return true if all positions are filled', function(){ + expect(game3.board.positions.length).toEqual(9); + + expect(game3.gameOver()).toBeTruthy(); + }); + + it('should return false if any positions are empty string', function(){ + game3.board.positions[1] = " "; + + expect(game3.gameOver()).toBeFalsy(); + }); + }); + + describe('winHorizontal', function(){ + var game4 = new Game(); + it('should return false if matches are empty strings', function(){ + expect(game4.winHorizontal()).toBeFalsy(); + }); + + it('should return true if there is a horizontal win in the first row', function(){ + game4.board.positions = ["X","X","X","O"," ","O","O","O","X"]; + expect(game4.winHorizontal()).toBeTruthy(); + expect(game4.get('winner')).toEqual(game4.get("player1")); + }); + + it('should return true if there is a horizontal win in the second row', function(){ + game4.board.positions = ["X","X"," ","O","O","O","X"," ","X"]; + game4.turn = game4.get("player2"); + expect(game4.winHorizontal()).toBeTruthy(); + expect(game4.get('winner')).toEqual(game4.get("player2")); + }); + + it('should return true if there is a horizontal win in the third row', function(){ + game4.board.positions = ["X","X","O","O"," ","O","X","X","X"]; + game4.turn = game4.get("player1"); + expect(game4.winHorizontal()).toBeTruthy(); + expect(game4.get('winner')).toEqual(game4.get("player1")); + }); + + it('should return false if there is no horizontal win yet', function (){ + game4.board.positions = ["X"," "," ","O"," "," "," "," "," "]; + expect(game4.winHorizontal()).toBeFalsy(); + }); + }); + + describe('winVertical', function() { + var game5 = new Game(); + it('should return false if matches are empty strings', function(){ + expect(game5.winVertical()).toBeFalsy(); + }); + + it('should return true if there is a vertical win in the first column', function(){ + game5.board.positions = ["X"," ","X","X"," ","O","X","O","X"]; + game5.turn = game5.get("player1"); + expect(game5.winVertical()).toBeTruthy(); + expect(game5.get('winner')).toEqual(game5.get("player1")); + }); + + it('should return true if there is a vertical win in the second column', function(){ + game5.board.positions = [" ","X"," ","O","X","O","O","X"," "]; + game5.turn = game5.get("player1"); + expect(game5.winVertical()).toBeTruthy(); + expect(game5.get('winner')).toEqual(game5.get("player1")); + }); + + it('should return true if there is a vertical win in the third column', function(){ + game5.board.positions = [" "," ","X","O"," ","X","O","O","X"]; + game5.turn = game5.get("player1"); + expect(game5.winVertical()).toBeTruthy(); + expect(game5.get('winner')).toEqual(game5.get("player1")); + }); + + it('should return false if there is no vertical win yet', function (){ + game5.board.positions = ["X"," "," ","O"," "," "," "," "," "]; + expect(game5.winVertical()).toBeFalsy(); + }); + }); + + describe('winDiagonal', function(){ + var game5 = new Game(); + it('should return false if matches are empty strings', function(){ + expect(game5.winDiagonal()).toBeFalsy(); + }); + + it('should return true if there is a diagonal win left to right', function(){ + game5.board.positions = ["X"," ","O","X","X","O","X","O","X"]; + game5.turn = game5.get("player1"); + expect(game5.winDiagonal()).toBeTruthy(); + expect(game5.get('winner')).toEqual(game5.get("player1")); + }); + + it('should return true if there is a diagonal win right to left', function(){ + game5.board.positions = [" ","X","O","O","O","X","O","X"," "]; + game5.turn = game5.get("player2"); + expect(game5.winDiagonal()).toBeTruthy(); + expect(game5.get('winner')).toEqual(game5.get("player2")); + }); + + it('should return false if there is no diagonal win yet', function (){ + game5.board.positions = ["X"," "," ","O"," "," "," "," "," "]; + expect(game5.winDiagonal()).toBeFalsy(); + }); + }); + + describe('gameWin', function(){ + var game6 = new Game(); + it('should return false at game start', function(){ + expect(game6.gameWin()).toBeFalsy(); + }); + + it('should return true if a win in any direction', function(){ + game6.board.positions = ["X","X","O","O"," ","O","X","X","X"]; + expect(game6.gameWin()).toBeTruthy(); + + game6.board.positions = [" "," ","X","O"," ","X","O","O","X"]; + expect(game6.gameWin()).toBeTruthy(); + + game6.board.positions = [" ","X","O","O","O","X","O","X"," "]; + expect(game6.gameWin()).toBeTruthy(); + }); + }); + + describe('takeTurn', function(){ + var game = new Game(); + + it('should throw an error if the position is not valid', function(){ + game.board.positions[0] = "X"; + expect(function(){ + game.takeTurn(0);}).toThrow(new Error('that position is already taken')); + }); + + it('should return the winner if someone won', function(){ + game.board.positions = [" "," "," ","O"," ","X","O","O","X"]; + expect(game.takeTurn(2)).toEqual(game.get("player1")); + }); + + it('should return gameOver if no one wins', function(){ + game.board.positions = ["O","X","X","X","X","O","O","O"," "]; + expect(game.takeTurn(8)).toEqual("gameOver"); + }); + + it('should toggle the turn if nobody won and game is not over', function(){ + game.board.positions = ["X"," "," ","O"," "," "," "," "," "]; + expect(game.turn).toEqual(game.get("player1")); + game.takeTurn(2); + expect(game.board.positions).toEqual(["X"," ","X","O"," "," "," "," "," "]); + expect(game.turn).toEqual(game.get("player2")); + }); + }); +}); diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..57ebf8b --- /dev/null +++ b/src/app.js @@ -0,0 +1,23 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import Backbone from 'backbone'; + +import Game from 'app/models/game'; +import Games from 'app/collections/games'; +import GamesView from 'app/views/games_view'; +// import Board from 'app/models/board'; +// import BoardView from 'app/views/board_view'; +import GameView from 'app/views/game_view'; + +$(document).ready(function(){ + + var games = new Games(); + var gameListView = new GamesView({ + el: $('body'), //this will be something else later. + model: games + }); + + games.fetch(); + console.log(games); + +}); diff --git a/src/app/collections/games.js b/src/app/collections/games.js new file mode 100644 index 0000000..aca6700 --- /dev/null +++ b/src/app/collections/games.js @@ -0,0 +1,14 @@ +//this is the collection of games, specifically for interacting with the API +import Backbone from 'backbone'; +import Game from 'app/models/game'; + +var Games = Backbone.Collection.extend({ + model: Game, + url: 'http://localhost:3000/api/v1/games', + parse: function(data) { + return data; + } + +}); + +export default Games; diff --git a/src/app/models/board.js b/src/app/models/board.js new file mode 100644 index 0000000..e6f1155 --- /dev/null +++ b/src/app/models/board.js @@ -0,0 +1,34 @@ +import Backbone from 'backbone'; + +const Board = Backbone.Model.extend({ + initialize: function(){ + this.positions = []; + for(var i = 0; i < 9; i++){ + this.positions.push(" "); + } + }, + + validPlay: function (position) { + if (!(Number.isInteger(position))) { + throw new Error('Input must be an integer between 0 and 8'); + } else if (this.positions[position] == " ") { + return true; + } else { + return false; + } + }, + + markPlay: function (mark, position) { + if(position === undefined) { + throw new Error('Wrong number of arguments'); + } else if(this.validPlay(position)) { + this.positions[position] = mark; + return position; + } else { + throw new Error('that position is already taken'); + } + } + +}); + +export default Board; diff --git a/src/app/models/game.js b/src/app/models/game.js new file mode 100644 index 0000000..f04aae8 --- /dev/null +++ b/src/app/models/game.js @@ -0,0 +1,131 @@ +import Backbone from 'backbone'; + +import Board from 'app/models/board'; + +const Game = Backbone.Model.extend({ + defaults: { + player1: "X", + player2: "O", + isOver: false, + winner: null + }, + + initialize: function(options){ + this.board = new Board(); + + //starting game with turn being equal to player 1 (X) + this.turn = this.get("player1"); + }, + + + toggleTurn: function () { + if (this.turn == this.get("player1")) { + this.turn = this.get("player2"); + } else if (this.turn == this.get("player2")) { + this.turn = this.get("player1"); + } + return this.turn; + }, + + gameOver: function () { + for (var i = 0; i < this.board.positions.length; i++) { + if(this.board.positions[i] == " "){ + return false; + } + } + this.set("isOver", true); + this.set('winner', "draw"); + return true; + }, + + winHorizontal: function () { + for(var i = 0; i < this.board.positions.length; i += 3){ + if ((this.board.positions[i] == this.board.positions[i+1]) && (this.board.positions[i] == this.board.positions[i+2])){ + if(this.board.positions[i] != " "){ + this.set('winner',this.turn); + return true; + } + } + } + return false; + }, + + winVertical: function () { + for(var i = 0; i < 3; i++) { + if((this.board.positions[i] == this.board.positions[i+3]) && (this.board.positions[i] == this.board.positions[i+6])) { + if(this.board.positions[i] != " ") { + this.set('winner', this.turn); + return true; + } + } + } + return false; + }, + + winDiagonal: function () { + // both diagonals use index 4, so check to make sure that it's not an empty string, and if it is, return false + if(this.board.positions[4] == " "){ + return false; + } + + // this is the left to right diagonal + if((this.board.positions[0] == this.board.positions[4]) && (this.board.positions[0] == this.board.positions[8])) { + this.set('winner', this.turn); + return true; + } + + // this is the right to left diagonal + if((this.board.positions[2] == this.board.positions[4]) && (this.board.positions[2] == this.board.positions[6])) { + this.set('winner', this.turn); + return true; + } + return false; + }, + + gameWin: function () { + if(this.winVertical() || this.winHorizontal() || this.winDiagonal()) { + this.set("isOver", true); + return true; + } else { + return false; + } + }, + + takeTurn: function (position) { + //this.turn is whose turn it is + this.board.markPlay(this.turn, position); + + //let that exception fly! + // toggle turn unless someone has won or game is over. + if (this.gameWin()) { + this.saveGame(); + return this.get('winner'); + } else if (this.gameOver()) { + this.saveGame(); + return 'gameOver'; + } else { + this.toggleTurn(); + } + }, + + toJSON: function() { + + var output = { + players: [this.get('player1'), this.get('player2')], + outcome: this.get('winner'), + board: this.board.positions + }; + + return output; + }, + + saveGame: function(){ + var outcome = this.get('winner'); + this.set('outcome', outcome); + console.log("saveGame got called"); + + this.trigger('createGame', this); + // this.create(rawGame); + } +}); +export default Game; diff --git a/src/app/views/board_view.js b/src/app/views/board_view.js new file mode 100644 index 0000000..f92afef --- /dev/null +++ b/src/app/views/board_view.js @@ -0,0 +1,42 @@ +import Backbone from 'backbone'; +import Board from 'app/models/board'; + +const BoardView = Backbone.View.extend({ + initialize: function(options) { + // this will actually come from the model, and not have anything in it at initialize. + this.positions = this.model.positions; + }, + + render: function() { + const boardList = this.$('#board-display'); + boardList.empty(); + + for(var i=0; i < this.positions.length; i++) { + // this should probably be a template + var square = "
  • " + this.positions[i] + "
  • "; + boardList.append(square); + } + + console.log(this.positions); + + // reattach dom even listeners to our brand spanking new HTML + this.delegateEvents(); + + return this; + }, + + + events: { + 'click li': 'markPosition' + }, + + markPosition: function(event) { + this.trigger('userPlay', {model: this.model, position: event.currentTarget.id}); + + console.log('markPosition called'); + this.render(); + } + +}); + +export default BoardView; diff --git a/src/app/views/game_view.js b/src/app/views/game_view.js new file mode 100644 index 0000000..fdd5ea2 --- /dev/null +++ b/src/app/views/game_view.js @@ -0,0 +1,80 @@ +import Backbone from 'backbone'; +import $ from 'jquery'; +import Game from 'app/models/game'; +import Board from 'app/models/board'; +import BoardView from 'app/views/board_view'; + + +const GameView = Backbone.View.extend({ + initialize: function(options) { + var playBoard = this.model.board; + + var board = new BoardView({ + el: $('.board'), + model: playBoard + }); + this.banner = this.$('.end-of-game'); + + this.listenTo(board, 'userPlay', this.turnPlay); + this.model.on('change:isOver', this.showBanner, this); + + board.render(); + }, + + render: function() { + + return this; + }, + + events: { + // 'click button': 'restartGame' + }, + + turnPlay: function(options){ + console.log("turnPlay!"); + console.log(JSON.parse(options.position)); + + // if no one has won yet or game is not over, call takeTurn. + if(!(this.model.gameWin() ||this.model.gameOver())){ + this.model.takeTurn(JSON.parse(options.position)); + } else { + // else, print something to the screen saying game is over. + console.log("You can't keep playing, the game is over!"); + } + }, + + showBanner: function(options){ + console.log("showBanner!"); + console.log(options.attributes.winner); //this is the winner, should use to populate the html. + //set the winner in the html + var message = $('#win-banner'); + + message.empty(); + + if (options.attributes.winner !== null){ + message.append(options.attributes.winner + " is the winner!"); + }else { + message.append("It's a draw."); + } + this.banner.show(); + }, + + // restartGame: function(event) { + // // this probably needs to happen up a level from here - cannot destroy all the things from within here. + // console.log('restartGame called'); + // this.model.board.destroy(); + // this.model.destroy(); + // var game = new Game(); + // var newGame = new GameView({ + // el: ('#game'), + // model: game + // }); + // newGame.render(); + // console.log(game.get("isOver")); + // + // } + + +}); + +export default GameView; diff --git a/src/app/views/games_view.js b/src/app/views/games_view.js new file mode 100644 index 0000000..1b568ad --- /dev/null +++ b/src/app/views/games_view.js @@ -0,0 +1,58 @@ +import Backbone from 'backbone'; +import $ from 'jquery'; +import Game from 'app/models/game'; +import GameView from 'app/views/game_view'; + + +const GamesView = Backbone.View.extend({ + + initialize: function(){ + + var game = new Game(); + + var gameview = new GameView({ + el: '#game', + model: game + }); + gameview.render(); + + this.listenTo(game,'createGame', this.createGame); + this.listenTo(this.model, 'add', this.addGame); + }, + + events: { + 'click #show-all-games': 'showGames' + }, + + addGame: function() { + console.log("add game getting called on the collection"); + + }, + + createGame: function(options){ + console.log('create game here'); + console.log(options); + + this.model.create(options); + console.log(this.model); + }, + + showGames: function(){ + // var button = this.$('#show-all-games'); + + var list = this.$('#prev-games'); + + for(var i = 0; i < this.model.length; i++){ + console.log(this.model.models[i].attributes); + var game = this.model.models[i].attributes; + + var tabledata = '' + game.id + ' Date/Time: ' + game.played_at + ' Winner: ' + game.outcome + ''; + list.append(tabledata); + } + + this.render(); + }, + +}); + +export default GamesView;