Watch: Screencast (24 minutes)Play: Game
Write: Another board game? Go? Checkers?
Transcript of screencast:
In the last screencast we setup a ClojureScript project,
implemented a tic tac toe game, and published it online.
Today I want to show you how to work with a larger codebase.
We are going to convert our tic tac toe game into Othello,
focusing on our workflow along the way.
Othello is played on an 8 by 8 board with black and white discs.
Each player places their discs in such a way as to capture the opponent's discs.
When placing a disc, opponent discs that become surrounded are captured.
Captured discs are converted to the capturing player's color by flipping them.
Before we get coding let's add testing feedback to our workflow.
Don't worry, this won't be the boring style of testing you may be expecting!
But it will take some setup to get going.
First add a dependency to our project.clj
Devcards is a library that showcases our components in various states.
Copy the dev build section to create a devcards build.
Give it a different output directory,
so that the compiled files do not collide with other builds.
Create a devcards.html file,
Copy and paste the contents from the devcards README on github.
Start the new build we configured by typing:
`lein figwheel devcards`
Seeing this page indicates that everything is working so far.
In the core.cljs source file, add devcards to the namespace require.
Refer the `defcard` macro.
Now define a card with some text in it.
The text can contain markdown; you can create titles and even format code.
Refer deftest from devcards instead of test.
See how our existing tests are now rendered into the page.
Failing tests are colored red.
Instead of `defcard`, we will be using a more specific version `defcard-rg`
which renders reagent components.
We can define a card that shows a single piece of the board in a given state.
Now we can isolate a single component to work on,
instead of having to observe the entire application.
We need a little more space here...
Let's show our existing pieces,
and adjust the sizing.
O.K. let's make some discs.
We'll use blue instead of white for now so that we can see the disc on a white background.
We need to pass the coordinates to place them in the right location.
Now we need a green background.
Let's copy the existing blank piece but make it green.
And the put blank pieces behind our discs.
That looks about right.
Let's parameterize the blank component so that it takes the background color.
We'll add an argument, but keep a default of grey if only 2 parameters are passed.
Does it work if we pass red?
Let's make a card with a complete board.
We don't need the title and button, so let's extract just the board.
We'll change our convention slightly so that blank cells on the board are represented with a space,
then we can use a B for black disc and W for white disc.
Update the render cases to default to a blank.
The top left nine positions are currently being initialized to B,
because that was our tic tac toe starting condition.
Let's make a function to create a `new-othello-board`.
An Othello game starts with four pieces in the center in a diagonal configuration.
Now we will add another card to show the tic tac toe game.
We need to update `new-tictactoe-game` to use space instead of B.
Notice that we now have two cards that show the same component in two different states.
Adding cards and tests reduces our time spent clicking through the application while developing.
Our blank cells don't do anything when clicked,
because they are modifying the global app-state.
We'll make them update the state passed instead.
There is quite a bit of logic in this click handler, let's extract that into a function.
We'll need to pass a game atom through, so that it can be modified.
Checking the console for errors reveals that we forgot to pass an atom to our original card.
We can use an empty map atom for those, as they need not respond to clicks.
Our game atom needs a status, because we prevented further moves after a win or draw.
Great, we can now click on the tic tac toe board and see the game progress.
When we click on the othello board, we are still using tic tac toe rules.
It is time to implement the othello rules.
The objective in othello is to capture the opponent's discs by surrounding them.
Here are the valid moves white can make in response to my move.
Let's start with detecting which cells positions are valid moves.
And placing a disc instead of a naught.
To preserve the tic tac toe rules we need two separate logic paths that dispatch on the game type.
Multimethods are a good way to express this.
You can think of multimethods as case statements as functions.
`defmulti` declares a name and the dispatch function.
`defmethod` declares a function to call for a dispatch value.
`defmulti` needs to have the same signature as the methods.
To start with, let's constrain the othello `can-move?` function to cells that are adjacent to a white disc.
To dispatch by type, we need to give our games a type.
Let's convert our `new-board` function into a `new-game` function,
returning a map containing the game type, status and board.
Oh no, something went wrong:
`Uncaught Error: 4 is not ISeqable.`
Expand the stacktrace by clicking the triangle at the start of the error message.
Scan down the stacktrace to the 4th row:
`tictactoe$core$available_QMARK_` Means in the namespace `tictactoe.core`, in the function `available?`
`@ core.cljs?rel:96` is the filename and line number.
:96 tells us the problem is on line 96 of our core.cljs source file.
The line above indicates that the problem occurred when calling `get-in` from the ClojureScript core namespace.
Ah yes, get-in is expecting a vector containing i and j. Fixed.
Player moves are limited now, but we still need to apply the same restrictions to the computer's moves.
Let's rearrange a little so that we can call the same function from `computer-move`.
Put the `board-size` inside the game.
And the `background-color`.
After these changes, the board-size is not coming through to the `game-board`.
Aha... `game-board` takes an atom, so we need to dereference the atom to pull out the values we are interested in.
But the computer is still not making moves.
Let's print out what `can-move?` is doing...
That all looks fine.
Aha... we should be updating the game board here.
We can make `computer-move` more generic.
Convert `can-move?` to return the game that would result from the move.
Now `computer-move` is just the first successful result of `can-move?`,
when examining cells in a shuffled order.
The computer is now placing white discs when we place a black disc.
`can-move?` needs to be parameterized to behave differently depending upon who is making the move.
The movement rule for othello is more than adjacency.
An othello move is one that captures opponent discs between your move and one of your existing discs.
Captured discs are flipped over and converted to your color.
To check for a valid move we check for capture lines in every direction.
A capture line is a sequence of the opponents disc that is terminated with one of your discs.
Let's write a test to see if this definition is correct.
Given a board "space W W W B", placing a B at 0 0 should capture the 3 W discs,
capture should return the coordinates 1 0, 2 0, 3 0.
Given a board "B W space W B", placing a B at 2 0 should capture the 2 W discs on either side,
capture should return the coordinates 1 0, 3, 0.
We need to pass a game to our `computer-move` test.
Now we need the next game state to contained the result of flipping all the captured discs.
Hmmm looks like I missed the terminating disc requirement.
Let's add a failing test to capture that.
Given a board "space W W space", placing a B at 0 0 should not return any capture coordinates.
`capture-line` needs to collect a sequence of potential flips,
but only return them when we encounter a terminating disc.
Great, the move rules are in place now.
If we ignore the `win?` condition, it looks very much like othello.
Let's rearrange `win?` to be game specific.
Let's convert it into a multimethod on game type.
In othello, the player with the most pieces at the end of the game wins.
Players must make a capturing move if possible, or pass.
We can use the `frequencies` function to discover the count of space, W and B in the board.
`apply concat` flattens the board from a 2 by 2 matrix into a one dimensional sequence.
If there are no remaining spaces, the player is a winner if they have more discs than the opponent.
In a 2 by 2 game board, B B B W is a win condition for black.
Now that we have several tests, it is inconvenient to scroll through the page to view them.
Let's get some output in the console to alert us on failures.
In our `project.clj` in the devcards build under figwheel, add an on-jsload hook.
Add the test directory to source-paths, and create a source file run.cljs under test/tictactoe
`run` will call ClojureScript's `run-all-tests` function.
Well also define a method to print out success or fail when the tests complete.
Now we can see a summary of the tests in the browser console whenever we change code.
In othello it is possible to reach a configuration where neither player can move,
but not all cells are taken yet.
Let's extract an `available-moves` function so we can use it in both the `computer-move` and `win?` condition.
Hmmm even though we extracted it, our current `win?` code is much higher up in the file,
so we will need to rearrange our functions to get the order of declaration correct.
This is a symptom of a larger issue:
There is a lot of code in core.cljs now!
Let's think about how to split the code up into more coherent namespaces.
Clearly we have Tic Tac Toe specific code and Othello specific code.
`can-move?` and `win?` will go in there.
And we might create in the future some other game implementations.
Our core namespace should be kept pretty small.
Core will be focused on connecting a game state with the desired logic and a view.
We'll need a central definition of the logic of a game,
in which we can declare the multimethods which will be implemented by the more specific namespaces.
It will contain the code that is common across games.
We have a bunch of component drawing code which we can put in a view namespace.
Tic Tac Toe, Othello, and other implementations will depend on Game,
as they need the defmulti to connect methods to.
Core will depend on Game and View, and the implementations of `new-game`.
View won't need to depend on anything because it will just be rendering data.
O.K. let's move these functions to the appropriate namespaces!
We need a draw? condition.
By printing out the game in the console, we can see victory is reached.
Let's check tic tac toe is still working.
I think we are all set, let's play our game!
So far we have been running the devcards build,
We want to switch over to viewing the dev build.
In your terminal, interrupt the current build by pressing "control c".
Now run both builds simultaneously, by typing`lein figwheel dev devcards`.
Navigate to localhost:3449 to see the dev build.
We only want to mount the main game when viewing the dev build,
so let's put that inside a conditional.
Let's play our game!
Hmmm this is interesting, I cannot move, but the computer has already moved.
In this situation the computer should move again in the last remaining cell.
We can achieve this by making check-game-status recurse while there is no player move available.
Let's add a test to make sure our logic works.
Here is a small 3 by 3 board where there are 2 computer moves and no player moves.
Let's play some more games.
I think the victory message should be on a different line from the buttons.
A strong strategy when playing othello is to focus on positions instead of how many discs you can flip.
The most advantageous positions in othello are the corners,
which can never be lost and allow you to recapture long lines in many directions.
Next best are the sides, for the same reason.
When starting a game it is better to capture fewer discs,
This limits your opponents options, and allows you to force your way into a side or corner.
Once you can establish the safety of a side and corner, you will have many opportunities to reclaim the central discs.
Time to publish our boardgames.
Our project source files are now core, game, othello, tictactoe and view.
And we added a devcards.html file to the resources directory.
I created a new repository for the combined games,
so that there is a separate repository for each video.
But you don't need to.
I'll rename my output file to othello.js to differentiate it from my previous project.
Let's check that the minified build still works.
Interrupt the current build with "control c"
and type `lein cljsbuild once min`
open index.html if you want to check it locally.
Commit and push to the gh-pages branch.
Navigate to username.github.io/project to see your page.
We converted our original game into two games.
The othello namespace contains our game specific rules for othello:
An othello game has a green background, the player is B, and computer is W.
The board has four discs in a diagonal configuration at the center.
Valid moves capture one or more lines of discs of the other player.
A draw occurs if there are no available moves to the player or computer.
We can shorten this test by using a new-board instead of repeating the values.
A win occurs when neither player can move, and one has more discs.
The core namespace has the game selection view and attaches the rendering.
The view namespace has the blank, circle, cross, discs, and game-board component.
The game namespace declares the multimethods that will dispatch by game type,
and some logic functions which are common between games.
The tictactoe namespace contains implementations specific to Tic Tac Toe.
We have a minified build which is published on github pages,
a dev build, and a devcards build.
Devcards presents cards and tests that occur in our namespaces,
so that we can see what our components will look like in predefined states.
We added an on-jsload hook to report a test summary to the console while developing.
If you have any questions, please drop a comment.
Until next time, keep coding!