This project is part of the object-oriented programming curriculum at App Academy. This implemenation is by Juliet Shafto and Yongho Kim.
The game works and all win conditions are accounted for, and the state of teh game is preserved when the page is refreshed. Token drop is not animated.
- In
index.html
- Add the "is-invisible" class to #board-holder.
- Add the "disabled" attribute to #new-game.
- In connect-four.js: Add an event handler for the window's "DOMContentLoaded" event. In that handler, have it:
- At the top of the file, declare a global variable named
game
and set it toundefined
. - Create an event handler for the "keyup" event of
#player-1-name
. In the event handler, have it set #new-game's "disabled" property to false if both #player-1-name and #player-2-name have non-empty content, enable #new-game. Otherwise, disable #new-game. - Create an event handler for the "keyup" event of
#player-2-name
. In this event handler, do the exact same thing you did in the#player-1-name
handler. Now, think, did you copy and paste some code? If so, can you refactor it somehow to remove duplication because the intended behavior is identical? - Create an event handler for the "click" event of
#new-game
that, when clicked:- Sets the global variable game to a new instance of the Game class passing in the two players' names.
- Sets the values of the two player name input elements to empty strings.
- Sets the disabled property on
#new-game
totrue
, thereby disabling it. (See if you have this functionality already written somewhere and, if you do, somehow reuse it so prevent code duplication.) -[x] Calls a function namedupdateUI()
.
- Declare a function named
updateUI()
after thegame
variable declaration and before the event listener for "DOMContentLoaded". In that function, put the following logic:- If
game
isundefined
, have it add the "is-invisible" class to#board-holder
. - If
game
is notundefined
have it remove the "is-invisible" class from#board-holder
and set the inner HTML of the#game-name
element to the value returned by thegetName()
function of the object stored in thegame
variable.
- If
- At the top of the file, import the
Game
class from the file at the path./game.js
usingimport { Game } from './game.js'
; because you want to load a file that will contain theGame
class. Remember that you have to use the ".js" on the file name because you're loading these directly in the browser.
- At the top of the file, declare a global variable named
- Create a file named
game.js
in the same directory as theconnect-four.js
file. In that file, declare and export a class namedGame
that has- A constructor that takes the names of the two players and sets them to instance variables on the object. (Remember that creating instance variables requires the use of the
this
keyword, such asthis.name1 = playerOneName
;, for example, creates an instance variable namedname1
and sets it to the value ofplayerOneName
.) - A method named
getName()
that returns a string of "Player 1 Name vs. Player 2 Name".
- A constructor that takes the names of the two players and sets them to instance variables on the object. (Remember that creating instance variables requires the use of the
- In game.js:
- In the constructor of the
Game
class, create a new instance variable to track the current player and set it to1
to indicate that its the first player. - Create a method called
playInColumn()
that will handle the click of the user. In that method, change the value of the current player to the other player. If it was1
, then make it2
; otherwise, make it1
.
- In the constructor of the
- In connect-four.js:
- In the event handler for "DOMContentLoaded", register an event handler for the "click" event on
#click-targets
. - In the
#click-targets
event handler that you just created, have it call theplayInColumn
method of the object in the global variablegame
. Then, have it call theupdateUI
method. - In the
updateUI
method, in the portion of the code that uses thegame
object, have it get the current player from the instance variable that you created in the constructor of theGame
class. Use that value to determine if you need to add "red" and remove "black" from the#click-targets
element for player two, or if you need to add "black" and remove "red" for player one from that element.
- In the event handler for "DOMContentLoaded", register an event handler for the "click" event on
- In the column.js file:
- Create and export a class named
Column
. - Create a constructor that will create a way to manage the tokens stored in the column.
- Create a method named
add
that takes a player number and stores it in the "bottom-most" entry in the column. - Create a method named
getTokenAt
which takes a row index number between0
and5
and returnsnull
if there's no token there,1
if player one's token is there, or2
if player two's token is there.
- Create and export a class named
- In game.js:
- Add to the
Game
constructor a new instance variable namedcolumns
and initialize it to an array of sevenColumn
objects. - Add a method named
playInColumn
that takes the index of the column in which to play, uses that index to select the correct column from the array ofcolumns
, and calls theadd
method on that column object passing in the number of the current player. Make sure that you leave the toggling of the current player from one to two and back, again, in the method at the end of it. - Add a method named
getTokenAt
that takes the row index and the column index. Use the column index to get the correct column from thecolumns
array. Then, call thegetTokenAt
method on the column object passing in just the row number. Return the return value of that function.
- Add to the
- In connect-four.js, in the event handler for click targets that you already have, before the call to the
playInColumn
method,- Parse the number of the click target that the player clicked on.
- Make sure your event handler is getting the event object in its parameter list.
- Access the "id" property of the "target" property of the click event.
- If it's a click that you want to handle, make sure that
id
value starts with the string "column-". - If it does, then use
Number.parseInt
to convert the last character of the id into a number. Pass that number into theplayInColumn
method.
- If it's a click that you want to handle, make sure that
- Parse the number of the click target that the player clicked on.
- In the
updateUI
method, it's now time to show the tokens in the board. Create afor
loop that will loop through the values from zero to five, inclusive; that will be the row index. Then, inside that for-block, create anotherfor
loop that loops from the values zero to six, inclusive; that will be the column index. Now, you have a row index and a column index to use to update the board. Inside the inner loop:- Select the element
#square-«row»-«column»
using the row and column indexes that you have. - Use the
getTokenAt
method on theGame
object stored in the globalgame
variable. The value that gets returned fromgetTokenAt
will determine what you should do:- First, clear out the inner HTML of the square you selected in the previous step by setting it to an empty string
- If the value returned by
getTokenAt
is1
, then create a "div" element, make sure it has both the "token" and "black" classes, and add it as the child to the square. - If the value returned by
getTokenAt
is2
, then create a "div" element, make sure it has both the "token" and "red" classes, and add it as the child to the square.
- Select the element
-
In the Column class:
- Depending on how you implemented it, just don't add the token if there is no available slot for it.
- Add a method named
isFull
and have it returntrue
if there are no more available slots (that is, there are already six tokens in it).
-
Add an
isColumnFull
method that takes a column index between0
and5
, inclusive, and returns the value of theisFull
method invoked on the appropriateColumn
object stored in thecolumns
array. -
In the updateUI method, create a for loop that iterates over the values from 0 to 6, inclusive. For each value:
- Select the element with the id of "column-«column index»".
- If the value returned from the
isColumnFull
method on theGame
object istrue
, then add the "full" class to the element selected in the previous step. - If the value returned from the
isColumnFull
method on theGame
object isfalse
, then remove the "full" class to the element selected in the previous step.
- Check if all of the columns are full, using the
isFull
method on eachColumn
object.- Put this behavior in the
Game
class. - Declare an instance variable in the constructor named
winnerNumber
. You'll set that to1
if Player One wins,2
if Player Two wins, and3
if both win (that is, it is a tie). SetwinnerNumber
in the constructor equal to0
to indicate that no one has won.
- Put this behavior in the
- Create a new file named column-win-inspector.js. In it, create and export a class named
ColumnWinInspector
.- Create a constructor that accepts a
Column
object.
- Create a constructor that accepts a
- Create a method named
inspect
that takes no parameters. In that method, have it check to see if the column has four contiguous tokens of the same player.- If they do, return that player's number. Otherwise, return
0
.
- If they do, return that player's number. Otherwise, return
- In the game.js file
- import the
ColumnWinInspector
class so that you can use it. - Create a new method in the
Game
class calledcheckForColumnWin
. Call it after you call thecheckForTie
method in theplayInColumn
method.- If the value of
winnerNumber
is already non-zero, skip the method. - In the
checkForColumnWin
, loop over every column in thecolumns
array.- For each column, create a new instance of the
ColumnWinInspector
class handing it the column that you're currently inspecting. - Call the
inspect
method on theinspector
object. If it returns1
or2
, then there's a winner. Set the value ofwinnerNumber
to the value and stop inspecting
- For each column, create a new instance of the
- If the value of
- import the
- Update the name of the game:
- In the
getName
method, if thewinnerNumber
is1
, return the message "«Player one name» wins!". If thewinnerNumber
is2
, return the message "«Player two name» wins!". Otherwise, just return the "tie" message or the "vs" message like before.
- In the
- Make it so the columns are "full"
- If there is a winner, it's as if all of the columns are full. In the
isColumnFull
method, check to see ifwinnerNumber
is1
or2
. If so, just returntrue
.
- If there is a winner, it's as if all of the columns are full. In the
- For the new class:
- Create a file named row-win-inspector.js.
- Create and export a class named
RowWinInspector
. - Declare the constructor to have a
columns
parameter that should contain four column objects. Save them in an instance variable. - Create an
inspect
method (with no parameters) that loops through the six rows and checks the token at each row in each of the columns. If any of the rows have identical entries in all four areas, then return the value of that winner's number. Otherwise, if there is no winner, return0
.
- In
Game
class:- At the end of the
playInColumn
method, call a new method, this time namedcheckForRowWin
. - Create the
checkForRowWin
method in theGame
class. - If the value of
winnerNumber
is already non-zero, skip the method. - Slice the
columns
array with groups of columns 0 - 3, 1 - 4, 2 - 5, and 3 - 6. - For each of those slices, create a new instance of the
RowWinInspector
class. - Call the
RowWinInspector
class'inspect
method. If the return value is greater than0
, then set the winner number and break.
- At the end of the
- Create an inspector just for the diagonals.
- Do the same kind of column splicing.
- Check the diagonals in both directions inside the slice. Return the value if you find four entries.
- Hook it up into the
Game
the same way that you didcheckForTie
,checkForColumnWin
, andcheckForRowWin
.
- Create and export a
GameJsonSerializer
class. - Have its constructor take a
Game
object. - The serializer should only talk directly to the
Game
object.- get the data out and store it in a way that you can put it back into a
Game
object using only the methods and properties available to you, namely:getTokenAt(rowIndex, columnIndex)
player1Name
player2Name
playInColumn(columnIndex)
- get the data out and store it in a way that you can put it back into a
- Declare and export a class named
GameJsonDeserializer
which has a constructor that accepts the JSON string. - It should have a
deserialize
method that callsJSON.parse
on the JSON string. - Then, it should create a new instance of the
Game
class and pass in the save player names to the constructor. - Then, using the
playInColumn
method, you need to restore the state of the board. Once you've done that, return that configuredGame
object.
- Use the
GameJsonSerializer
at the end of the handler for the click targets. Create a new serializer by passing in theGame
object into its constructor, callserialize
on it, and save the returned string into local storage. - Use the
GameJsonDeserializer
as the last line of the "DOMContentLoaded" handler.- It should read a value from local storage and, if that value's not
null
, then create an instance of thedeserializer
and pass the string into its constructor. - Call the
deserialize
method and set the global game variable to the return value. - Call
updateUI
because you now have a game!
- It should read a value from local storage and, if that value's not
Right now, when a player clicks above the column, it just shows up at the appropriate place in the board. It's up to you to now animate that so that it from above the board down to its final resting place in the column. While the token is animating its fall, the game should not process any clicks.
You can do this a few different ways. Here are some hints that you can use exclusively or in combination with one another.
- You can try to use CSS transitions to have the CSS "move" the token for you.
- You can try using a third-party library like tween to "more" the token for you.
- You can use CSS positioning and write a JavaScript setInterval to "move" the token yourself.