Reversi / Othello
Due Jan 31st, by 11PM
A B C D E F G H +---+---+---+---+---+---+---+---+ 1 | | | | | | | O | | +---+---+---+---+---+---+---+---+ 2 | | | | | | O | | | +---+---+---+---+---+---+---+---+ 3 | | | | O | O | X | | | +---+---+---+---+---+---+---+---+ 4 | | | X | O | X | X | | | +---+---+---+---+---+---+---+---+ 5 | | | | X | X | | | | +---+---+---+---+---+---+---+---+ 6 | | | X | | X | | | | +---+---+---+---+---+---+---+---+ 7 | | X | | | | | | | +---+---+---+---+---+---+---+---+ 8 | | | | | | | | | +---+---+---+---+---+---+---+---+
Overview
Description
Create an interactive 2-player (computer vs human player) Reversi/Othello game.
- check out the rules on wikipedia
- you can check out this strategy guide if you're interested in Reversi strategy
- you'll write this game in two parts…
- create several helper functions for your game
- implement the game using some of the helper functions you made for part 1
- it'll be a bit over-engineered
- but don't worry - you don't have to use all of the functions you create
See the example game shown at the end of these instructions.
Objectives
- Write some JavaScript!
- control structures
- functions
- Array and string manipulation
- Learn how to run node programs
- Learn about node built-ins:
process
exports
require
- Install and use modules, create your own
- Run unit tests to check your work
- Use a static analysis tool (ESLint) to help prevent bugs / errors
- Use the fs module to read a file / handle asynchronous I/O
- Parse JSON
Grading
- all parts of the homework count toward your grade
- there will be a significant penalty for not adding the config file and scripted moves portion
- these parts are at the end of the instructions (see
User Controlled Game Settings
andHandling Scripted Moves
) - the implementation of these features is important because they help both you and the graders test your program
- these parts are at the end of the instructions (see
Submission Process
The final version of your assignment should be in GitHub.
- push your changes to the homework repository on GitHub
- repositories will be closed on the due date and time so that no more commits can be pushed!
- after the due date, no further commits will be seen by the graders
Preparation
Ensure that node and npm are installed (this should have been done for homework #0). You should be able to open up your terminal or DOS Shell and run node -v
and npm -v
. Both commands should output a version number (probably something like 6.2.2
for node and 3.9.5
for npm).
- use git / clone the repository
- install development modules
- mocha and chai for running the supplied unit tests
- eshint for cleaning up your JavaScript / spotting common sources of bugs and errors in your code
- install modules required by your game
Use Git / Clone the repository
Make sure you have git / a git client!
Assuming that you've already:
- submitted your github username via the form/survey
- accepted the invitation that adds you to the github organization for this course
You can then go through the following steps to clone your repository and commit your first changes:
- …go to the class github page
- find the repository that starts with your NYU NetID and ends with homework01 (for example, jjv222-homework01)
- on the repository's page, use the green "Clone or download" button on the right side of the screen to copy the HTTPS clone URL to clone the homework. To use the commandline client (with GITHUB_REPOSITORY_URL being the url you copied from the green button):
git clone GITHUB_REPOSITORY_URL
- create a file called
.gitignore
in the same directory - add the following line to the file so that git ignores any locally installed node modules:
node_modules
- in the same project directory, create a file called README.md, and edit it so that it includes:
- your name and net id
- the title of your project: Homework #01 - Reversi
- again, in the same project directory, run
git add README.md
to let git know that we're ready to "save" - save your work locally by running
git commit -m "first commit"
… everything within the quotes after-m
is any commit message you'd like - finally, send your work to github by running
git push
(orgit push origin master
)
Install Development Modules
You'll have to install a couple of node modules to help you run tests and use static analysis tools on your code. These tools won't be required for your program to run, but they will be useful while you're writing your programs.
You'll be installing the following module globally:
mocha
- for running unit tests
You'll also install the following modules locally in your project directory:
chai
- supplies assertions for unit testseslint
- for catching potential errors in your code
Go into the directory of your cloned repository (cd username-homework01
), and run the following commands:
npm install -g mocha
npm install --save-dev eslint
npm install --save-dev chai
Note that the last commands install modules locally to your project directory. It will do two things:
- It will make a modification to an existing file,
package.json
, within your project folder - It will create a
node_modules
folder where your downloaded modules are stored (this folder is included in your.gitignore
file because these external libraries are not meant to be in your project's version control
Install Required Modules
You'll also need a module to help you ask the user for input.
- in your repository directory, install the node module,
readline-sync
, by running this command in your project's directory:npm install --save readline-sync
- note that the
readline-sync
module allows you to prompt for user input synchronously- this is very different from how node.js apps usually operate
- however, for our purposes, using sync prompt is fine (for now), and it mimics the browser's prompt functionality well
- check out the example usage on readline-sync's npm page
- essentially:
var readlineSync = require('readline-sync');
- which imports the function
question
from thereadline-sync
module into your program
- essentially:
- note that installing
readline-sync
will make a modification topackage.json
as well. This modification topackage.json
should be committed and pushed as well!
Minimum Number of Commits
As you write your code, make sure that you make at least four commits total (more commits are better; if you can, try to commit per feature added).
- the commits should be meaningful (that is, do not just add a newline, commit and push to make up the requirements for commits).
- make sure your commit messages describe the changes in the commit; for example:
add config file reader and set board based on config file
fix bug that prevented vertical lines of tiles from being flipped
git add --all
git commit -m 'your commit message'
- push your code frequently
git push
Running Your Programs
To run your programs, use the commandline (Terminal.app, DOS, etc.):
# in your project directory
# change directory to src folder
cd src
node myfile.js
# or, without changing directory
node src/myfile.js
Part 1 - Reversi Functions and Running Unit Tests
Background
For your implementation of Reversi, you'll break down the game into several functions. These helper functions will be written in a module (a file separate from the file that actually runs your game), which you'll use in part 3: src/reversi.js
The helper functions you'll be implementing are described below. Unit tests have been included in your repository in the file, tests/reversi-test.js
.
Creating a Module / Exporting Functions
You'll be creating a module that contains a bunch of helper functions. The file that you'll be writing your module in is already included in your repository in src/reversi.js
. Both your actual interactive Reversi game (in Part 2) and the supplied unit tests will use this module.
To make the functions you write available when your module is brought into another program (that is, required or imported), you'll have to export your functions. See this sitepoint tutorial or this article to get a primer on modules, exports
and using require
. There are a few ways to do export your functions (all of the examples use module.exports
, but they should work with just exports
as well):
- create all of your functions … then, at the end, assign
module.exports
to an object literal containing all of the functions that you want to export:function repeat(ele, n) { // implementation } function generateBoard(rows, cols, initialValue) { // implementation } // ... module.exports = { repeat: repeat, generateBoard: generateBoard, // ... }
- create all of your functions in an object and assign that object to
module.exports
:const rev = { repeat: function(value, n) { // implementation }, generateBoard: function(rows, columns, initialCellValue) { // implementation }, // ... } module.exports = rev;
- Create functions as properties on
module.exports
module.exports.repeat = function(value, n) { // implementation } module.exports.generateBoard = function(rows, columns, initialCellValue) { // implementation }, // ...
When you require
your module, the object you create for exports
will be given back. In the example below, the module, some-module.js
is brought in to the current file (the ./
specifies that the file is in the same directory as the current file) and is represented by the variable, foo
. The functions can be accessed by using regular dot notation on the foo
object:
var foo = require('./some-module.js');
foo.someFunction();
You should make sure your exports are up to date as you implement your functions so that you can run your unit tests as you complete your function implementations.
Unit Tests
You can use the supplied unit tests (in tests/reversi-test.js
) to check that your functions are:
- are named correctly
- have the required parameters
- return the appropriate value(s)
- meet the minimum requirements according to the specifications
The given unit tests use Mocha as a testing framework and Chai for assertions. While you don't have to know how to write these tests, you should read through them (the api is very human readable) to get a feel for how your functions are being tested. If you're curious about writing unit tests, check out this article on codementor.
You can run the included unit tests by using this command in your project directory:
mocha tests/reversi-test.js
If you run these tests before starting, you'll get a bunch of reference errors. This is because you have no functions implemented yet. Additionally, you'll have to export the functions you create so that the tests have access to them.
Please try continually running the unit tests as you develop your program. To clear out the noise, feel free to comment out the tests that you aren't working on, and uncomment them as soon as you have a stub of a function exported.
Assumptions
The functions make some assumptions about how you'll be representing a Reversi board.
- although some functions allow for arbitrary rows and columns…
- you can assume that a board will always have at least 16 squares (4 x 4), but no more than 26 x 26 squares
- you can also assume that a board's rows and columns will always be equal
- we can name a cell / square based on its row number and column number
- rows start from the top with row number 0
- cols start from the left with column number 0
- the diagram below shows a 4 X 4 board with row and column labels
columns 0 1 2 3 +---+---+---+---+ 0 | 0 | 1 | 2 | 3 | +---+---+---+---+ r 1 | 4 | 5 | 6 | 7 | o +---+---+---+---+ w 2 | 8 | 9 | 10| 11| s +---+---+---+---+ 3 | 12| 13| 14| 15| +---+---+---+---+
- in this example, the lower right most square (containing 15) is at row 3, column 3
- the cell containing 9 is at row 2, column 1
- Alternatively, we can reference a cell using a format borrowed from chess algebraic notation
- the columns are uppercase letters starting with A from the left
- … while the rows are numbers starting at 1 from the top
- in this notation the column (letter) is first, followed by the row (number)
- using this notation, the board with labels would like like this:
columns A B C D +---+---+---+---+ 1 | 0 | 1 | 2 | 3 | +---+---+---+---+ r 2 | 4 | 5 | 6 | 7 | o +---+---+---+---+ w 3 | 8 | 9 | 10| 11| s +---+---+---+---+ 4 | 12| 13| 14| 15| +---+---+---+---+
- the lower right most square, containing 15, is at D4
- the cell containing 9 is B3
- Although a 2-dimensional Array is a natural fit for representing a Reversi board, your implementation of Reversi will use a one dimensional
Array
to represent the board- in this representation, imagine all of the rows of the board placed adjacent to each other
+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ row, col | 0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3 | 3,0 | 3,1 | 3,2 | 3,3 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ algebraic | A1 | B1 | C1 | D1 | A2 | B2 | C2 | D2 | A3 | B3 | C3 | D3 | A4 | B4 | C4 | D4 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
- the lower right most square, D4 (row 3, col 3) is at index 15
- B3 (row 2, col 1) is index 9
- in this representation, imagine all of the rows of the board placed adjacent to each other
- The space character,
" "
, will be used to mark an empty square - Reversi uses black and white pieces (discs) … these pieces will be represented by:
- the letter
X
for black pieces - the letter
O
for a white piece
- the letter
- You can assume that all of the code examples below use
rev
as the name of the imported module of your Reversi functions
Functions to Implement
Note that:
- most functions will return values (to make it easier to test)
- the tests are meant to ensure that your functions are named correctly and work for a few simple cases
- they don't cover all of the corner cases / requirements
- so check out the tests before running to see what else you may need to check for
- feel free to augment the tests with your own additional tests
repeat(value, n)
Parameters:
value
- the value to be repeatedn
- the number of times to repeat thevalue
Returns:
- an
Array
containingn
elements, with each element beingvalue
Description:
repeat
creates an Array
that contains value
as each element for n
elements. If the value
is a reference type, it's ok if the reference is copied (that is, you don't have to worry about deep copying value
if it's an Object
.
Hint: The Array
methods, push and/or spread/concat may be helpful here.
Example:
const arr = rev.repeat("hello", 3);
// arr is ["hello", "hello", "hello"];
generateBoard(rows, columns, initialCellValue)
Parameters:
rows
- the number of rows in the boardcolumns
- the number of columns in the boardinitialCellValue
- the initial value contained in each square- default value should be space (
" "
)
- default value should be space (
Returns:
- a single dimensional
Array
containing the number of elements that would be in a rows x columns board… with each cell containing the initial value,initialCellValue
Description:
Creates a single dimensional Array
representation of a Reversi board. The number of elements in the Array
is the same as the number of squares in the board based on the supplied number of rows
and columns
. The initial value in each cell is the initialCellValue
passed in
Use the repeat
function that you created above to implement this function.
Example:
// creates a board with 16 squares
const board = rev.generateBoard(4, 4);
// board is = [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "];
// (each empty cell is a space because the default value is space)
rowColToIndex(board, rowNumber, columnNumber)
Parameters:
board
- the board where therowNumber
andcolumnNumber
come fromrowNumber
- the row number to be converted to an index in a one dimensional aArray
representationcolumnNumber
- the column number to be converted to an index in a one dimensional aArray
representation
Returns:
- a
Number
, the index that's mapped to by the givenrowNumber
andcolumnNumber
Description:
A cell in a Reversi board can be specified by a row number and a column number. However, our board implementation uses a one dimensional Array
, so a cell must be specified by a single index. This function translates a row and a column into an index in the one dimensional Array
representation of a Reversi board.
Hint: Math.sqrt can be used to help determine the original width and height of the board even though board
comes in as a one dimensional Array
.
Example:
// translates a row and col to a single index
const board = rev.generateBoard(3, 3, " ");
const i = rev.rowColToIndex(board, 1, 1);
const j = rev.rowColToIndex(board, 0, 2);
// i is 4 (column 1, row 1 is the same as the element at index 4)
// j is 2
indexToRowCol(board, i)
Parameters:
board
- the board where therowNumber
andcolumnNumber
come fromi
- the index to be converted into a row and column
Returns:
- an object containing two properties,
row
andcol
, representing the row and column numbers that theindex
maps to
Description:
Translates a single index in a one dimensional Array
representation of a board to that cell's row and column. The board
supplied can be used to determine the max column and row numbers. You can assume that the board is always square. Row and column numbers start at 0.
Hint: Again, Math.sqrt can be used to determine the original width and height of the board. If you need integer division or if you need to always round down, you can just call Math.floor after dividing (which will give you the largest integer less than or equal to a given number).
Example:
// translates an index into a row and column...
const board = rev.generateBoard(3, 3, " ");
const rowCol1 = rev.indexToRowCol(board, 4);
const rowCol2 = rev.indexToRowCol(board, 2);
// rowCol1 is: {"row": 1, "col": 1};
// rowCol2 is {"row": 0, "col": 2};
setBoardCell(board, letter, row, col)
Parameters:
board
- the board where a cell will be set toletter
letter
- the string to set the cell torow
- the row number of the cell to be setcol
- the column number of the cell to be set
Returns:
- a single dimensional
Array
representing the board where the cell atrow
andcol
is set to the value ofletter
Description:
Sets the value of the cell at the specified row and column numbers on the board, board
, to the value, letter
without mutating the original board passed in.
Do this by creating a shallow copy of board
and modifying it. Return the copy instead of the original board
passed in. To shallow copy an Array
, use the spread operator or the slice method.
Example:
// sets a single square, but does not mutate the original board passed in
const board = rev.generateBoard(3, 3, " ");
const updatedBoard = rev.setBoardCell(board, "X", 1, 1);
// board is [" ", " ", " ", " ", " ", " ", " ", " ", " "]
// updatedBoard is [" ", " ", " ", " ", "X", " ", " ", " ", " "]
algebraicToRowCol(algebraicNotation)
Parameters:
algebraicNotation
- aString
that specifies the position of a cell using algebraic notation
Returns:
- an object containing two properties,
row
andcol
, representing the row and column numbers that thealgebraicNotation
maps to (for example,{"row": 1, "col": 1}
) undefined
if the algebraic notation passed in is not valid.
Description:
Translates algebraic notation specifying a cell into a row and column specifying the same cell. If the notation passed in is not valid, then return undefined
.
The algebraic notation format we'll use will be a single string with the column letter first, immediately followed by the row number (with nothing separating the row and column). The column letter starts at letter A and the row number starts at index 1 (which is different from row and column notation above where numbers start at 0). You can assume that there are no more than 26 rows and columns. Some examples of valid formats: A1
and C20
. Some invalid formats include: 1A
, 1
, A
, A:1
, ***
.
Hint: There are many ways you can implement this. If you want to loop through every character, you can use a simple for loop, the string's length, and the string's index operator ([]
) or charAt method. Note that there's no actual character type, you just get a String
composed of a single character.
Alternatively, you can create an Array
containing every character of the original String
by using split with an empty string as an argument.
Once you can look at each individual character (or group of characters), you can examine each character by using isNaN to determine if a String
is not numeric.
Finally if you feel like wrangling regular expressions, you can try using the match method.
Example:
// valid
rev.algebraicToRowCol("B3") // for a 4 x 4 board, {"row": 2, "col": 1}
rev.algebraicToRowCol("D4") // for a 4 x 4 board, {"row": 3, "col": 3}
// not valid:
rev.algebraicToRowCol("A")) // undefined
rev.algebraicToRowCol("2")) // undefined
rev.algebraicToRowCol("2A")) // undefined
rev.algebraicToRowCol(" ")) // undefined
rev.algebraicToRowCol("A 2")) // undefined
rev.algebraicToRowCol("A:2")) // undefined
rev.algebraicToRowCol("**")) // undefined
placeLetters(board, letter, algebraicNotation)
Parameters:
board
- the board where a cell will be set toletter
letter
- the string to set the cell to- one or more:
algebraicNotation
- aString
that specifies the position of a cell using algebraic notation
Returns:
- a single dimensional
Array
representing the board where the cells at eachrow
andcol
specified is set to the value ofletter
Description:
Translates one or more moves in algebraic notation to row and column… and uses the row and column to set the letter specified on the board.
Use the setBoardCell
function you created above to implement this. Consequently, the incoming board should not be mutated (instead, copy it, modify the copy and return the modified copy).
Use rest parameters to handle multiple arguments this. Again, the original board passed in should not be changed (instead, copy it, modify the copy, and return the copy).
Example:
// X and O are placed on the board
let board = rev.generateBoard(4, 4, " ");
board = rev.placeLetters(board, 'X', "B3", "D4");
// board is [" ", " ", " ", " ", " ", " ", " ", " ", " ", "X", " ", " ", " ", " ", " ", "X"]
// index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// note that placeLetters can be called with an arbitrary number of moves
// ...instead of calling with two moves, the line below calls it with four
// rev.placeLetters(board, 'X', "B3", "D4", "A1", "A2");
boardToString(board)
Parameters:
board
- the board to be converted to aString
Returns:
- a
String
representation of the board
Description:
Creates a text drawing representation of the Tic Tac Toe board
passed in. The board should have:
- borders between cells
- the contents of each cell
- labels on the rows and columns
Printing out an example result would yield:
A B C D
+---+---+---+---+
1 | | | | |
+---+---+---+---+
2 | | O | X | |
+---+---+---+---+
3 | | X | O | |
+---+---+---+---+
4 | | | | |
+---+---+---+---+
It should work for boards of any size! Here's an example of a 7 x 7 board!
A B C D E F G H
+---+---+---+---+---+---+---+---+
1 | | | | | | | O | |
+---+---+---+---+---+---+---+---+
2 | | | | | | O | | |
+---+---+---+---+---+---+---+---+
3 | | | | O | O | X | | |
+---+---+---+---+---+---+---+---+
4 | | | X | O | X | X | | |
+---+---+---+---+---+---+---+---+
5 | | | | X | X | | | |
+---+---+---+---+---+---+---+---+
6 | | | X | | X | | | |
+---+---+---+---+---+---+---+---+
7 | | X | | | | | | |
+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+---+---+---+---+---+---+---+---+
This one is actually quite challenging (and tedious) to get exactly right, so there won't be any penalties for minor spacing inconsistencies… and if there are other issues (for example, not adding labels), there will only be small point penalties.
Again, you can assume that the numbers of rows and columns will not be greater than 26.
Hint: One way of dealing with the row label is to use String.fromCodePoint, which gives you the unicode code point of the character supplied.
Example:
// a string representation of a board with two moves
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "B2");
board = rev.placeLetter(board, 'O', "C1");
// board will be equal to the following string:
" A B C \n +---+---+---+\n 1 | | | O |\n +---+---+---+\n 2 | | X | |\n +---+---+---+\n 3 | | | |\n +---+---+---+\n";
isBoardFull(board)
Parameters:
board
- the board to examine
Returns:
true
if there are no empty cells left in the board,false
otherwise
Description:
Examines the board
passed in to determine whether or not it's full. It returns true
if there are no empty squares, false
if there are still squares available. Assume that the board uses the space character, " "
, to mark a square as empty.
Hint: The solution to this is pretty straightforward with a simple for loop, but if you want to ditch the for loop entirely, you can get fancy and use the Array
method, some.
Example:
// board is empty
let board = rev.generateBoard(3, 3, " ");
console.log(rev.isBoardFull(board)); // --> false
// board is completely full
board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'X', "A2");
board = rev.placeLetter(board, 'X', "A3");
board = rev.placeLetter(board, 'X', "B1");
board = rev.placeLetter(board, 'X', "B2");
board = rev.placeLetter(board, 'X', "B3");
board = rev.placeLetter(board, 'X', "C1");
board = rev.placeLetter(board, 'X', "C2");
board = rev.placeLetter(board, 'X', "C3");
console.log(rev.isBoardFull(board)); // --> true
// board has one square empty...
board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A2");
board = rev.placeLetter(board, 'X', "A3");
board = rev.placeLetter(board, 'X', "B1");
board = rev.placeLetter(board, 'X', "B2");
board = rev.placeLetter(board, 'X', "B3");
board = rev.placeLetter(board, 'X', "C1");
board = rev.placeLetter(board, 'X', "C2");
board = rev.placeLetter(board, 'X', "C3");
console.log(rev.isBoardFull(board)); // --> false
flip(board, row, col)
Parameters:
board
- the board the move is performed onrow
- the row number of piece/letter to flipcol
- the column number of piece/letter to flip
Returns:
- a single dimensional
Array
representing the board where the letter atrow
andcol
is changed to the opposite letter - fromX
toO
orO
toX
(that is, the piece is changed to the opposite color)
Description:
Using the board
passed in, flip the piece at the specified row
and col
so that it is the opposite color by changing X
to O
or O
to X
. If no letter is present, do not change the contents of the cell.
Example:
board = rev.generateBoard(4, 4, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.flip(board, 0, 0);
// board[0] is now 'O'!
flipCells(board, cellsToFlip)
Parameters:
board
- the board the move is performed oncellsToFlip
- a 3DArray
representing groups of rows and columns
Returns:
- a single dimensional
Array
representing the board where the letters specified incellsToFlip
are changed to the opposite letter - fromX
toO
orO
toX
(that is, the pieces are changed to the opposite color)
Description:
Using the board
passed in, flip the pieces in the cells specified by cellsToFlip
.
cellsToFlip
is a 3 dimensional array:
- the inner most
Array
has 2 elements, a row and a column - groups of rows and columns are wrapped in an
Array
(these groupings are meant to represent lines of consecutive tiles) - finally, these groups are also wrapped in an
Array
For example:
[[[0, 0], [0, 1]], [[1, 1]]]
|_______________| |_____|
| |
group 1 (2 cells) group 2 (1 cell)
Each group represents a line of consecutive cells. Note, though, that these lines have no relevance to how the tiles are flipped - only that in order to get to the cell location, you'll have to descend through nested Arrays
.
Example:
board = rev.generateBoard(4, 4, " ");
board = rev.placeLetters(board, 'X', "A1", "B1", "B2");
board = rev.flipCells(board, [[[0, 0], [0, 1]], [[1, 1]]]);
// the letters at index 0, 1 and 5 are all set to 'O'!
getCellsToFlip(board, lastRow, lastCol)
Parameters:
board
- the board the move is performed onlastRow
- the row of the last move on the boardlastCol
- the column of the last move on the board
Returns:
Array
representing groups of rows and columns pairs that contain pieces that should be flipped to the opposite color/letter because of the last move played
Description:
From wikipedia:
(Assuming the player is X/Black/Dark) Dark must place a piece with the dark side up on the board, in such a position that there exists at least one straight (horizontal, vertical, or diagonal) occupied line between the new piece and another dark piece, with one or more contiguous light pieces between them.
After placing the piece, dark turns over (flips, captures) all light pieces lying on a straight line between the new piece and any anchoring dark pieces. All reversed pieces now show the dark side, and dark can use them in later moves—unless light has reversed them back in the meantime. In other words, a valid move is one where at least one piece is reversed.
Using the board
passed in determine which cells contain pieces to flip based on the last move. For example, if the last move was the X
played at D3
, then all of the O
's on the board would be flipped (D2
, B3
and C3
.
A B C D +---+---+---+---+ 1 | | | | X | +---+---+---+---+ 2 | | | | O | +---+---+---+---+ 3 | X | O | O | X | +---+---+---+---+ 4 | | | | | +---+---+---+---+
Consequently, for the board above, this function will return the row and column pairs representing the cells where pieces need to be flipped. These pairs are grouped by "lines". B3
and C3
are grouped together because they are in the same line. These groups are collected in an Array
…. and the returned Array
should look like:
[[[2, 1], [2, 2]], [1, 3]]
Again groups are formed by the lines: horizontal, vertical and diagonal. But really, this translates to 8 directions starting from the proposed move:
- left
- right
- up
- down
- upper left diagonal
- upper right diagonal
- lower left diagonal
- lower right diagonal
So for example, in the board setup below, if there were an X move to D4, there would be 8 groups, but only because there are 8 lines of cells to be flipped:
A B C D E F G H
+---+---+---+---+---+---+---+---+
1 | | | | | | | X | |
+---+---+---+---+---+---+---+---+
2 | | X | | X | | O | | |
+---+---+---+---+---+---+---+---+
3 | | | O | O | O | | | |
+---+---+---+---+---+---+---+---+
4 | | X | O | | O | O | X | |
+---+---+---+---+---+---+---+---+
5 | | | O | O | O | | | |
+---+---+---+---+---+---+---+---+
6 | | X | | X | | X | | |
+---+---+---+---+---+---+---+---+
7 | | | | | | | | |
+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+---+---+---+---+---+---+---+---+
Hint - a possible brute force algorithm to do this:
- For every possible straight line starting from the move (up, down, left, right, diagonal upper left, etc.):
- Check if the next cell contains your opponent's letter
- If it does, add it to a list of potential cell coordinates continue to the next cell in your line…
- If that cell contains your letter, you're done! …all of your collected cells are cells to flip
- If the next cell doesn't exist or the next cell is empty (a space) none of the cells you've collected can be flipped
Lastly, the ordering of the groups, as well as the cells in those groups can vary. Ordering does not matter! (This will depend on the algorithm that you use for this, so a specific ordering is not required.)
Example:
let board = rev.generateBoard(4, 4, " ");
board = rev.placeLetters(board, 'O', 'B3', 'C3', 'D2');
board = rev.placeLetters(board, 'X', 'A3', 'D1', 'D3');
const res = rev.getCellsToFlip(board, 2, 3); // last move/proposed move was D3
// res will look something like: [[[2, 1], [2, 2]], [[1, 3]]]
isValidMove(board, letter, row, col)
Parameters:
board
- the board the move is performed onletter
- the letter used for the intended moverow
- the row number of the intended movecol
- the column number of the intended move
Returns:
true
if the move is valid,false
otherwise
Description:
Using the board
passed in, determines whether or not a move with letter
to row
and col
is valid. A valid move:
- targets an empty square
- is within the boundaries of the board
- adheres to the rules of Reversi… that is, the piece played must flip at least one of the other player's pieces
Use a previous function that you created, rowColToIndex
in the implementation of this function. Again, you can assume that the space character, " "
, represents empty.
Example:
// a valid move
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'O', "A2");
rev.isValidMove(board, 'X', 2, 0); // true!
// not a valid move
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "B1"); // notice that this does not form a line!
board = rev.placeLetter(board, 'O', "A2");
rev.isValidMove(board, 'X', 2, 0); // false
// remember to check for:
// 1. out of bounds
// 2. a piece that already exists
isValidMoveAlgebraicNotation(board, letter, algebraicNotation)
Parameters:
board
- the board the move is performed onletter
- the letter to be placedalgebraicNotation
- algebraic notation representing the intended move on theboard
Returns:
true
if the intended move is valid,false
otherwise
Description:
Using the board
passed in, determines whether or not a move with letter
to algebraicNotation
is valid. Use the functions you previously created, isValidMove
and algebraicToRowCol
to implement this function.
Example:
// valid move
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'O', "A2");
rev.isValidMoveAlgebraicNotation(board, 'X', 'A3'); // true
getLetterCounts(board)
Parameters:
board
- the board that contains the pieces/letters to count
Returns:
object
with property names as letters and values as counts of those letters
Description:
Returns the counts of each of the letters on the supplied board
. The counts are stored in an object where the count is the value and the letter is the property name. For example, if the board has 2 X's and 1 O, then the object return would be: { X: 2, O: 1 }
Example:
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'X', "A3");
board = rev.placeLetter(board, 'O', "A2");
const counts = rev.getLetterCounts(board);
// counts is {X: 2, O: 1}
getValidMoves(board, letter)
board
- the board used for determining valid movesletter
- the piece/letter that valid moves will be determined for
Returns:
Array
- a 2-dimensionalArray
representing a list of row and column pairs, with each pair a valid move for theletter
provided
Description:
Gives back a list of valid moves that the letter
can make on the board
. These moves are returned as a list of row and column pairs - an Array
containing 2-element Array
s
Example:
// 1 valid move can be made for X on this board
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'O', "A2");
const res = rev.getValidMoves(board, 'X');
// [[2, 0]]
// no valid moves for X can be made on this board
let board = rev.generateBoard(3, 3, " ");
board = rev.placeLetter(board, 'X', "A1");
board = rev.placeLetter(board, 'O', "A3");
const res = rev.getValidMoves(board, 'X');
// []
// 2 possible valid moves can be made for X on this board
let board = rev.generateBoard(4, 4, " ");
board = rev.placeLetters(board, 'X', 'A1');
board = rev.placeLetters(board, 'O', 'B2');
board = rev.placeLetters(board, 'X', 'A2');
board = rev.placeLetters(board, 'O', 'C3');
const res = rev.getValidMoves(board, 'X');
// [[3, 3], [1, 2]]
Checking Your Code, Pushing Your Changes
- JavaScript is kind of crazy (read: has some really bad, but syntactically valid parts), so it's useful to use a static analysis tool, like
eslint
to check your code- ideally, you'd be doing this periodically while you write your program
- the commandline usage is described here, but there are eslint integrations for some editors (see the plugins section in the eslint installation guide
- from your project directory run:
./node_modules/.bin/eslint src/*
to check all of the code in thesrc
directory - (you did install
eslint
locally in the preparation section, right?) - check the output; make sure you fix all warnings / errors
- Run your tests one last time to make sure that they're all (or… mostly) passing.
mocha tests/reversi-test.js
- Fix unit test errors
- if you have test failures, examine the output of each failure…
- it'll describe what was expected vs what was actually given back by your function…
- +/green shows expected, while -/red shows the incorrect output
- if you get
TypeError ... is not a function
, you may have:- not implemented the function (!)
- named the function differently than what was specified in the instructions
- did not export the function from your module
- finally make sure your changes are saved and pushed
- use git to add and commit to continually save changes
- push your changes so that they're available on the remote repository (github)
Part 2 - Reversi / Othello
Whew. That was a lot of work. But, ummmm… there's no Reversi game yet. What? Let's use the module / helper functions you created in part 1 to implement an interactive Reversi game that supports the following features:
- User controlled game settings
- An interactive game
- A game configuration to automate settings, start the board with a predefined setup, and allow scripted moves for both the player and the opponent
You don't have to use all of the functions you created in your reversi.js
module. However, you'll likely end up doing a lot of redundant work if you don't!
Prep
You'll write your Reversi game in the file called src/app.js
. Your first step is to bring in some required modules. Open up src/app.js
and…
- bring in the module you created by using
require
// you can name the object whatever you like // "rev" is used below... var rev = require('./reversi.js');
- bring in the module,
readline-sync
, which you installed in the preparation portion of the homeworkvar readlineSync = require('readline-sync');
- also bring in the
fs
module for reading files </code></pre>
Read in a Config File / Commandline Arguments
Before starting the game, a few settings have to be configured. This can be done via a config file (which will also allow you to start with a board with pieces already on it) or by asking the player to specify values explicitly. The settings to configure are:
- width of the board
- the letter/color for the player (
X
is black,O
is white,X
goes first)
This section describes how a config file could be used to initialize a game. (Asking the player for settings will be covered in the next section).
- the config file will be specified as a commandline argument that's passed in to your program when running it through the commandline
- to access commandline arguments, use the built-in
process.argv
. It's anArray
that contains the data passed in to your program through the commandline.- see the official documentation
- the filename of the optional config file will be first and only argument that can be passed in
- … consequently, check for the existence of
process.argv[2]
- for example, if you run your file as
node src/app.js /Users/joe/Desktop/myconfig.json
- … then the string,
/Users/joe/Desktop/myconfig.json
, will be stored inprocess.argv[2]
- if a config file is passed in, read it!
- use the
fs
module's readFile function to do this - example usage of the
fs
module:fs.readFile('/path/to/myFile.txt', 'utf8', function(err, data) { if (err) { console.log('uh oh', err); } else { console.log(data); } });
- note that the 1st argument is a path to a file (can be relative or absolute)
- the 2nd argument is an optional encoding (in this case utf8),
- … and the 3rd argument is a function to call when the file is finished being read
- this callback function has 2 parameters:
- an
error
object that isundefined
if there's no error, or an object containing information on the error if there is an error - and the
data
contained in the file
- use the
- you can assume that the config file will be in JSON format
- use
JSON.parse
to take a string of JSON data and convert it into actual JavaScript objects - expect that the JSON file contains the following data:
- data for configuring the game
- a
String
representing the letter assigned to the player - an
Array
representing the board (optionally with pieces placed)
- a
- optionally, data to script the moves for the player and computer to aid in testing
- an
Array
of scripted moves in algebraic notation for the computer to use for moves - an
Array
of scripted moves in algebraic notation for the player to use for moves - if the above arrays of scripted moves are empty, then the player and computer can move freely
- an
- here's a sample config file:
{ "boardPreset": { "playerLetter": "X", "board": [ " ", " ", "X", " ", " ", " ", " ", " ", " ", " ", "O", " ", " ", " ", " ", " ", " ", " ", "O", " ", " ", " ", " ", " ", "X", "O", " ", "O", "X", "O", "O", "X", " ", " ", "O", "X", "O", " ", " ", " ", " ", " ", "O", " ", " ", " ", " ", " ", " ", " ", "X", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " " ] }, "scriptedMoves": { "player": ["C4", "F3"], "computer": ["E3", "F2", "B6"] } }
- use
- use the data in the JSON file to initialize your game (for example, use it to generate a board, set the player letter, etc.)
- from here you can start the interactive game
Of course, if no config file is specified, then ask the user for the game settings… (see below)
User Controlled Game Settings
If no config file is specified (or if the config file is not found) prompt the user to set up some game options. Use readline-sync
to ask the user for input synchronously (again, very different from how node usually works, but more in line with how we're accustomed to seeing how programs flow). Check out the documentation on readline-sync
on npm.
Here's some example usage:
const readlineSync = require('readline-sync');
const answer = readlineSync.question('What is the meaning of life?');
console.log(answer);
- greet the user by saying something like:
REVERSI?
- ask the user for the width of the game board
- the width must be at least 4 squares wide
- the width cannot be more than 26 squares wide
- the width must be even
- if the width does not fall within the above range or if the width entered is not numeric (remember, you can use
isNaN
for this) or not even, then ask the user for the width again - see the example interaction below (note that the first 3 answers are not valid widths, but the 4th valid answer allows the user to progress to the next question)
How wide should the board be? (even numbers between 4 and 26, inclusive) > blah How wide should the board be? (even numbers between 4 and 26, inclusive) > 3 How wide should the board be? (even numbers between 4 and 26, inclusive) > -12 How wide should the board be? (even numbers between 4 and 26, inclusive) > 4 Pick your letter: X (black) or O (white)
- ask the user what color they'd like to be; 'X' for black or 'O' for white)
- uppercase 'X' and uppercase 'O' are the only valid inputs
- if the user does not enter a valid letter, continually ask the user for a letter until a valid letter is given
- once the player has chosen a letter, output
Player is (color/letter chosen)
- see the example interaction below (the first 2 inputs are not valid - the first x is lowercase):
Pick your letter: X (black) or O (white) > x Pick your letter: X (black) or O (white) > asdf Pick your letter: X (black) or O (white) > X Player is X
- use the data collected to construct a board (use on of the functions that you created!) and show the letter that the player chose along with the empty board … initialize the board with 4 pieces in the center 4 squares as specified by the Reversi/Othello rules
Player is X A B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | | O | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ What's your move?
An entire happy path (that is, all valid input) interaction would look like this:
How wide should the board be? (even numbers between 4 and 26, inclusive)
> 4
Pick your letter: X (black) or O (white)
> X
Player is X
A B C D
+---+---+---+---+
1 | | | | |
+---+---+---+---+
2 | | O | X | |
+---+---+---+---+
3 | | X | O | |
+---+---+---+---+
4 | | | | |
+---+---+---+---+
What's your move?
>
An Interactive Game
Now… for the actual game. The user will be playing against the computer.
If the moves aren't scripted (that is, a config file was not used OR the config file contained empty arrays for computer and player moves)….
- Black (
X
) goes first (so if the user chose 'O', the computer will make the first move) - As long as the board isn't full and there hasn't been 2 consecutive passes….
- for the player's move, ask the player for a move in algebraic notation
- if the move is not valid (use one of the functions you wrote to determine this!), notify the user and ask for another move
Player is X A B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | | O | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ What's your move? > A1 INVALID MOVE. Your move should: * be in a
format * specify an existing empty cell * flip at elast one of your oponent's pieces What's your move? > A2 A B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | X | X | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Score ===== X: 4 O: 1 </code></pre>
- after a player moves, show the total counts for the player and computer
- if a player cannot make a valid move, tell the player to press
to pass instead of allowing the player to enter a move A B C D E F G H +---+---+---+---+---+---+---+---+ 1 | | | | | | | | | +---+---+---+---+---+---+---+---+ 2 | | | | | | | | | +---+---+---+---+---+---+---+---+ 3 | | | | X | | | | | +---+---+---+---+---+---+---+---+ 4 | O | O | X | X | X | O | O | X | +---+---+---+---+---+---+---+---+ 5 | | | | | | | | | +---+---+---+---+---+---+---+---+ 6 | | | | | | | | | +---+---+---+---+---+---+---+---+ 7 | | | | | | | | | +---+---+---+---+---+---+---+---+ 8 | | | | | | | | | +---+---+---+---+---+---+---+---+ No valid moves available for you. Press <ENTER> to pass.
- however if a player is able to move, once they complete their move, ask the player to press <ENTER> to allow the computer move (ask for any input again using something like
readlineSync.question('Press <ENTER> to show computer\'s move...');
… without storing the input in a variableA B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | X | X | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Score ===== X: 4 O: 1 Press <ENTER> to show computer's move...
- for the computer's move, you can use any algorithm you want to generate a valid move
- note that the computer can pass as well if it does not have any valid moves
- Once there are 2 consecutive passes from the player to computer or vice versa, the game ends
- Display who won based on the counts of the pieces/letters on the board (ties are possible)
Score ===== X: 11 O: 5 You won! 👍
- See the example game at the end of these instructions
Handling Scripted Moves
If there were scripted moves defined in the config file, allow the game to proceed by pulling the moves for the computer and player from these Arrays… and using them to make a move:
- if the move pulled from the Array is not valid, then ignore it and allow the computer or player to make their move manually
- otherwise, if the move is valid, then prompt the player to press
to see their next move - if there are no more moves to pull from the array of scripted moves, allow the computer or player to choose their move like usual
- Some more details:
- again, the player can move manually after the scripted moves have been exhausted
- if the scripted move is an invalid move, that scripted move is skipped (and the user or computer will move manually), and the next scripted move for that player (comp or user) will be used on that player's next turn
- execute the scripted moves based on who is 'X', so if the player is 'X', their scripted moves will start first (same as if playing interactively)
- there can be an uneven number of moves - if there are, follow this process: is there a scripted move to use? use it… otherwise move like you would normally (computer picks randomly or player enters move manually)
- for example using this configuration:
{ "boardPreset": { "playerLetter": "X", "board": [ " ", " ", " ", " ", "x", "O", "X", " ", " ", "X", "O", " ", " ", " ", " ", " " ] }, "scriptedMoves": { "player": ["A2", "D3"], "computer": ["C1", "A3"] } }
- the game would proceed as follows:
REVERSI Computer will make the following moves: [ 'C1', 'A3' ] The player will make the following moves: [ 'A2', 'D3' ] Player is X A B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | | O | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Player move to A2 is scripted. Press <ENTER> to continue.
A B C D +---+---+---+---+ 1 | | | | | +---+---+---+---+ 2 | X | X | X | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Score ===== X: 4 O: 1 Press <ENTER> to show computer's move...
Computer move to C1 was scripted. A B C D +---+---+---+---+ 1 | | | O | | +---+---+---+---+ 2 | X | X | O | | +---+---+---+---+ 3 | | X | O | | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Score ===== X: 3 O: 3 Player move to D3 is scripted. Press <ENTER> to continue.
A B C D +---+---+---+---+ 1 | | | O | | +---+---+---+---+ 2 | X | X | O | | +---+---+---+---+ 3 | | X | X | X | +---+---+---+---+ 4 | | | | | +---+---+---+---+ Score ===== X: 5 O: 2 Press <ENTER> to show computer's move...
Example Game
Animated gif of Reversi/Othello
Text Example
How wide should the board be? (even numbers between 4 and 26, inclusive)
> 4
Pick your letter: X (black) or O (white)
> X
Player is X
A B C D
+---+---+---+---+
1 | | | | |
+---+---+---+---+
2 | | O | X | |
+---+---+---+---+
3 | | X | O | |
+---+---+---+---+
4 | | | | |
+---+---+---+---+
What's your move?
> B1
A B C D
+---+---+---+---+
1 | | X | | |
+---+---+---+---+
2 | | X | X | |
+---+---+---+---+
3 | | X | O | |
+---+---+---+---+
4 | | | | |
+---+---+---+---+
Score
=====
X: 4
O: 1
Press <ENTER> to show computer's move...
A B C D
+---+---+---+---+
1 | | X | | |
+---+---+---+---+
2 | | X | X | |
+---+---+---+---+
3 | O | O | O | |
+---+---+---+---+
4 | | | | |
+---+---+---+---+
Score
=====
X: 3
O: 3
What's your move?
> B4
A B C D
+---+---+---+---+
1 | | X | | |
+---+---+---+---+
2 | | X | X | |
+---+---+---+---+
3 | O | X | O | |
+---+---+---+---+
4 | | X | | |
+---+---+---+---+
Score
=====
X: 5
O: 2
Press <ENTER> to show computer's move...
A B C D
+---+---+---+---+
1 | O | X | | |
+---+---+---+---+
2 | | O | X | |
+---+---+---+---+
3 | O | X | O | |
+---+---+---+---+
4 | | X | | |
+---+---+---+---+
Score
=====
X: 4
O: 4
What's your move?
> A2
A B C D
+---+---+---+---+
1 | O | X | | |
+---+---+---+---+
2 | X | X | X | |
+---+---+---+---+
3 | O | X | O | |
+---+---+---+---+
4 | | X | | |
+---+---+---+---+
Score
=====
X: 6
O: 3
Press <ENTER> to show computer's move...
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | O | O | |
+---+---+---+---+
3 | O | X | O | |
+---+---+---+---+
4 | | X | | |
+---+---+---+---+
Score
=====
X: 3
O: 7
What's your move?
> A4
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | O | O | |
+---+---+---+---+
3 | X | X | O | |
+---+---+---+---+
4 | X | X | | |
+---+---+---+---+
Score
=====
X: 5
O: 6
Press <ENTER> to show computer's move...
Computer has no valid moves. Press <ENTER> to continue
Score
=====
X: 5
O: 6
What's your move?
> D2
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | X | X | X |
+---+---+---+---+
3 | X | X | X | |
+---+---+---+---+
4 | X | X | | |
+---+---+---+---+
Score
=====
X: 9
O: 3
Press <ENTER> to show computer's move...
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | X | O | X |
+---+---+---+---+
3 | X | X | X | O |
+---+---+---+---+
4 | X | X | | |
+---+---+---+---+
Score
=====
X: 8
O: 5
What's your move?
> D4
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | X | O | X |
+---+---+---+---+
3 | X | X | X | X |
+---+---+---+---+
4 | X | X | | X |
+---+---+---+---+
Score
=====
X: 10
O: 4
Press <ENTER> to show computer's move...
A B C D
+---+---+---+---+
1 | O | O | O | |
+---+---+---+---+
2 | X | X | O | X |
+---+---+---+---+
3 | X | X | O | X |
+---+---+---+---+
4 | X | X | O | X |
+---+---+---+---+
Score
=====
X: 9
O: 6
What's your move?
> D1
A B C D
+---+---+---+---+
1 | O | O | O | X |
+---+---+---+---+
2 | X | X | X | X |
+---+---+---+---+
3 | X | X | O | X |
+---+---+---+---+
4 | X | X | O | X |
+---+---+---+---+
Score
=====
X: 11
O: 5
You won! 👍