Final result is visible here: https://vue-ttt-game.netlify.com/#/
- Git
- Node.js >= 12.16.1
- yarn >= 1.22.0
- Visual Studio Code with the Vetur extension
- Recent version of Google Chrome with the Vue.js devtools extension
yarn install
yarn serve
yarn test:unit
- The game is played on a grid that's 3 squares by 3 squares.
- The first player is X, the second player is O (both players are human).
- The first player to get 3 of her marks in a row (up, down, across, or diagonally) is the winner.
- When all 9 squares are full, the game is over.
- Clone the repository on your computer:
git clone https://github.com/nicolaspayot/vue-ttt-game.git
- Install the dependencies:
yarn install
- Start the development server:
yarn serve
- Go to http://localhost:8080 and open the devtools π
The square logic
Display an X when the player X clicks on an empty square, and display an O when the player O clicks on an empty square.
- Inspect
Board.vue
component in the Vue.js devtools and add some "X" and "O" to thesquares
data, see how the view automatically updates. - Refactor the
Board
component template to use av-for
loop instead of 9 hard coded squares. - To handle the logic of a square, create a
Square.vue
component with the following template:
<div class="game__box">
<span>{{ value }}</span>
</div>
- The
Square
component should contain a propvalue
and emits an event called"click"
when the user clicks on<div class="game__box">
. - The
Board
component should now use theSquare
component, pass the value of each square and react to the click events. - A click event should update the value of the given square ("X" or "O") and update the
player
. Usethis.$set(array, index, value)
so thatsquares
updates reflect on the view.
The game status
Display X and O with different colors, display the game status (next player, winner or draw game).
- In the
Square
component, on the root tag, dynamically add the classesgame__box--x
whenvalue
is "X" andgame__box--o
whenvalue
is "O". - Create a
Status.vue
component with the following template:
<template>
<div class="game__status">
<span>Next player is X</span>
</div>
</template>
-
The
Status
component should contain 3 props:winner: String
player: String
isDRawGame: Boolean
-
Use these props in the template to display:
- "Winner is X" (or "Winner is O") if there's a winner.
- "That's a draw game" if there's a draw game.
- "Next player is X" (or "Next player is O") if the game is still in progress
-
Use the
Status
component in theBoard
component just bellow the root tag :<div class="game">
and pass the required props. -
The
player
prop is the current player.winner
andisDrawGame
are 2 computed that you should implement.- A function to find the winner is provided in
src/utils/winner.js
.
- A function to find the winner is provided in
Highlight the winner
Improve the UI by highlighting the winner row and disabling the hover and click animations on empty squares when the game is over.
-
In the
Square
component, add 2 props:isGameOver: Boolean
isWinner: Boolean
-
Then, dynamically add the classes
game__box--over
when the game is over andgame__box--winner
when the square is in a winning combination. -
In
Board
component, create a new computedisGameOver
that returnstrue
when there's a winner or there's a draw game. Also, create a methodisSquareWinner
that returns true if the given square position is included in the winner positions. -
Finally, pass the computed and the method call as props to the
Square
component.
Restart the game
Improve the UX (User eXperience) by allowing users to restart the game without reloading the whole application.
- Create a
Restart.vue
component with the following template:
<template>
<div class="game__action">
<button>Restart game</button>
</div>
</template>
- This component should emit an event called
"click"
when the user clicks on the button. - In the
Board
component template, add theRestart
component on the last position inside the<div class="game">
tag. - Add a method
restartGame
that will reset the game on the click event of theRestart
component.
Routing and shared state
Add a lobby page before starting the game. In this lobby, you shall be able to name the players.
- Create a new component
Lobby.vue
that should allow you to set two player names. Use the following template:
<form class="lobby">
<div>
<label>Player 1</label>
<input type="text" placeholder="Name for player 1" />
</div>
<div>
<label>Player 2</label>
<input type="text" placeholder="Name for player 2" />
</div>
<div class="lobby__action">
<button type="submit">Start the game</button>
</div>
</form>
- The button "Start the game" should launch the game using the Vue navigation mechanism.
- Add the dependency
vue-router
to your package.json with the commandyarn add vue-router
. - Configure two paths for your app, one for the
Lobby
component and the other one to yourBoard
component. - Change
App.vue
to hold routing logic. - The app should now point by default to the Lobby component and the restart button should also bring you back to the Lobby.
- Add the dependency
vuex
to your package.json with the commandyarn add vuex
. - Add player names to the Vuex store to share these data across your components.
- The
Status
component can now retrieve player names from the store.
Unit Tests
Add unit tests to make sure that the game meets all expectations and to prevent breaking changes.
- Test the
winner
function by creating a spec file in/tests/unit/utils/winner.spec.js
with the following content:
import winner from "@/utils/winner";
import winningCombinations from "../fixtures/winning-combinations";
describe("winner function", () => {
it("should return no winner for empty squares", () => {
// Given
const squares = ...;
// When
const { player, positions } = winner(squares);
// Then
// Do expectations on player and positions
});
it("should return no winner for a draw game", () => {
// Given
const squares = ...;
// When
const { player, positions } = winner(squares);
// Then
// Do expectations on player and positions
});
it("should return X as a winner and the winning positions", () => {
// Given
winningCombinations.forEach(([squares, winningCombination]) => {
//When
const { player, positions } = winner(squares);
// Then
// Do expectations on player and positions
});
});
});
- To pass the tests, you need to:
- Update
squares
value for 1st and 2nd tests. - Add expectations on
player
andpositions
for the all tests
- Update
- Add more tests for the
Board.vue
component by updating the spec file in/tests/unit/components/Board.spec.js
with the following content:
describe("Board.vue", () => {
...
it("should be possible to play on an empty square when game is not over", () => {
// TODO
});
it("should not be possible to play on a non empty square when game is not over", () => {
// TODO
});
it("should not be possible to play when game has a winner", () => {
// TODO
});
it("should display the next player when game is not over", () => {
// TODO
});
it("should display the winner", () => {
// TODO
});
it("should display when game is a draw", () => {
// TODO
});
});