So far, we have seen some small programs. Now, we are going to create a larger software project: five-in-a-row. First, we define the "requirements", that is what the program must do:
We are going to build this program in steps ("increments"), that is we don't write the complete program at once. Instead, in each step we create some code that solves part of the requirements and can be demonstrated.
- five-in-a-row is a board game, with a board of 10 x 10 fields
- the game is played by 2 players
- the 1st player places a red dot in a field, the 2nd player places a blue dot, the 1st player a red dot, and so on
- a player can only place a dot in an empty field
- when a player has 5 dots in a row (horizontal, vertical or diagonal), the game is over
- the game is internet based, that is the game is running on a server and the players use a web browser to play the game
We are going to build this program in steps ("increments"), that is we don't write the complete program at once. Instead, in each step we create some code that solves part of the requirements and can be demonstrated.
Increment 1:
Because the game must be internet based, we use our previous chat_server.js and chat_client.html as base. We copy the files to 5_in_row_server.js and 5_in_row_client.html, and make the following changes:
Client:
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<canvas id="myCanvas" onclick="sendMove(event)" width="400" height="400"></canvas>
<p id="paragraph_1"> </p>
<script>
var p = document.getElementById("paragraph_1");
var c = document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var socket = io.connect("http://" + window.location.host);
drawBoard();
socket.on('status', function (col, row) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
ctx.beginPath();
ctx.arc(40*col+20, 40*row+20, 12,0,2*Math.PI);
ctx.fillStyle="#FF0000";
ctx.fill();
});
function sendMove(event) {
var col = Math.floor(event.clientX / 40);
var row = Math.floor(event.clientY / 40);
socket.emit('move', col, row);
}
function drawBoard() {
for (var i=0; i<11; i++) {
ctx.moveTo(i*40,0);
ctx.lineTo(i*40,400);
ctx.stroke();
ctx.moveTo(0,i*40);
ctx.lineTo(400,i*40);
ctx.stroke();
}
}
</script>
</body>
</html>
Server:
var app = require('http').createServer(handler)
, io = require('/home/bitnami/node_modules/socket.io/lib/socket.io.js').listen(app)
, fs = require('fs')
app.listen(8080);
io.set('log level',1);
function handler (req, res) {
fs.readFile('5_in_row_client.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading file');
}
res.writeHead(200);
res.end(data);
});
}
io.sockets.on('connection', function (socket) {
socket.on('move', function (col,row) {
io.sockets.emit('status', col,row);
});
});
Because the game must be internet based, we use our previous chat_server.js and chat_client.html as base. We copy the files to 5_in_row_server.js and 5_in_row_client.html, and make the following changes:
Client:
- replace the HTML elements input and button by a canvas (as in Programming for kids, part 5: more javascript), as the User Interface of the game is different then the User Interface of the chat
- add onclick="sendMove(event)" to the canvas, as the player clicks on the canvas instead of on a button
- replace the function sendChat by the function sendMove(event), as the clients report different things to the servers
- replace the socket.on('chat',function(data) { ... }); by socket.on('status',function(col,row) {...});, as the clients receive different things from the servers
- add the function drawBoard(), the create the board.
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<canvas id="myCanvas" onclick="sendMove(event)" width="400" height="400"></canvas>
<p id="paragraph_1"> </p>
<script>
var p = document.getElementById("paragraph_1");
var c = document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var socket = io.connect("http://" + window.location.host);
drawBoard();
socket.on('status', function (col, row) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
ctx.beginPath();
ctx.arc(40*col+20, 40*row+20, 12,0,2*Math.PI);
ctx.fillStyle="#FF0000";
ctx.fill();
});
function sendMove(event) {
var col = Math.floor(event.clientX / 40);
var row = Math.floor(event.clientY / 40);
socket.emit('move', col, row);
}
function drawBoard() {
for (var i=0; i<11; i++) {
ctx.moveTo(i*40,0);
ctx.lineTo(i*40,400);
ctx.stroke();
ctx.moveTo(0,i*40);
ctx.lineTo(400,i*40);
ctx.stroke();
}
}
</script>
</body>
</html>
Server:
- change the reference (fs.readFile) to the client file
- replace the socket.on('text',function(data) {...}); by socket.on('move',function(col,row) {...});, as the server receives different things from the clients, and reports different things to the clients.
var app = require('http').createServer(handler)
, io = require('/home/bitnami/node_modules/socket.io/lib/socket.io.js').listen(app)
, fs = require('fs')
app.listen(8080);
io.set('log level',1);
function handler (req, res) {
fs.readFile('5_in_row_client.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading file');
}
res.writeHead(200);
res.end(data);
});
}
io.sockets.on('connection', function (socket) {
socket.on('move', function (col,row) {
io.sockets.emit('status', col,row);
});
});
Increment 2:
The 1st increment gives a working program, although it is not yet complete. It is too limited: each player can click at any time, only the last click is displayed, always in red, etc.
In the 2nd increment, we define players and add color (red, blue) for each player.
Client:
socket.on('status', function (col, row,color) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
ctx.beginPath();
ctx.arc(40*col+20, 40*row+20, 12,0,2*Math.PI);
ctx.fillStyle=color;
ctx.fill();
});
Server:
var socket_red = null;
var socket_blue = null;
...
io.sockets.on('connection', function (socket) {
if (socket_red == null) { socket_red = socket.id; }
} else {
if (socket_blue == null) { socket_blue = socket.id; }
}
.....
io.sockets.on('connection', function (socket) {
socket.on('move', function (col,row) {
if (socket.id == socket_red) { io.sockets.emit('status', col,row,"#FF0000"); }
if (socket.id == socket_blue { io.sockets.emit('status', col,row,"#0000FF"); }
});
The 1st increment gives a working program, although it is not yet complete. It is too limited: each player can click at any time, only the last click is displayed, always in red, etc.
In the 2nd increment, we define players and add color (red, blue) for each player.
Client:
- The socket.on('status', function (col, row) is extended with a color. This color is used to fill the circle.
socket.on('status', function (col, row,color) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
ctx.beginPath();
ctx.arc(40*col+20, 40*row+20, 12,0,2*Math.PI);
ctx.fillStyle=color;
ctx.fill();
});
Server:
- To identify the players (sockets), two variables are defined: socket_red and socket_blue. They are set when a connection is made. In the io.socket.on('connection', function (socket), the connected players are identified
- In the socket.on('move', function (col,row), the player that did send the move is identified.
- The io.sockets.emit('status', col,row) is extended with a color.
var socket_red = null;
var socket_blue = null;
...
io.sockets.on('connection', function (socket) {
if (socket_red == null) { socket_red = socket.id; }
} else {
if (socket_blue == null) { socket_blue = socket.id; }
}
.....
io.sockets.on('connection', function (socket) {
socket.on('move', function (col,row) {
if (socket.id == socket_red) { io.sockets.emit('status', col,row,"#FF0000"); }
if (socket.id == socket_blue { io.sockets.emit('status', col,row,"#0000FF"); }
});
Increment 3:
In the 2nd increment only the last click is displayed. In the 3rd increment, we show all clicks.
Server:
var status = new Array();
...
function move(col,row,color) {
this.col=col;
this.row=row;
this.color=color;
}
....
socket.on('move', function (col,row) {
if (socket.id == socket_red) {
status[status.length]=new move(col,row,"#FF0000");
io.sockets.emit('status', JSON.stringify(status));
}
if (socket.id == socket_blue) {
status[status.length]= new move(col,row,"#0000FF");
io.sockets.emit('status', JSON.stringify(status));
}
}
Client:
socket.on('status', function (statusStr) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
var status = JSON.parse(statusStr);
for (var i=0; i< status.length; i++) {
ctx.beginPath();
ctx.arc(40*status[i].col+20, 40*status[i].row+20,12,0,2*Math.PI);
ctx.fill[i].color;
ctx.fill();
}
});
In the 2nd increment only the last click is displayed. In the 3rd increment, we show all clicks.
Server:
- The object move is defined.
- The server keeps a list of moves in the variable status.
- When a 'move' is received, the variable status is updated with this move.
- The message 'status' contains a list of moves, instead of just one row and col.
- JSON is used to send the status.
var status = new Array();
...
function move(col,row,color) {
this.col=col;
this.row=row;
this.color=color;
}
....
socket.on('move', function (col,row) {
if (socket.id == socket_red) {
status[status.length]=new move(col,row,"#FF0000");
io.sockets.emit('status', JSON.stringify(status));
}
if (socket.id == socket_blue) {
status[status.length]= new move(col,row,"#0000FF");
io.sockets.emit('status', JSON.stringify(status));
}
}
Client:
- The client shows all clicks instead of only the last one.
socket.on('status', function (statusStr) {
ctx.clearRect(0, 0, c.width, c.height);
drawBoard();
var status = JSON.parse(statusStr);
for (var i=0; i< status.length; i++) {
ctx.beginPath();
ctx.arc(40*status[i].col+20, 40*status[i].row+20,12,0,2*Math.PI);
ctx.fill[i].color;
ctx.fill();
}
});
Increment 4:
In the increments so far, each click as accepted. In the 4th increment, we check whether a click is OK. Also we inform the players whose turn it is.
Server:
socket.on('move', function (col,row) {
if (socket.id == socket_red &&
socket_blue != null &&
(status.length % 2 == 0)) {
status[status.length]=new move(col,row,"#FF0000");
io.sockets.socket(socket_red).emit('message',
'waiting for blue');
io.sockets.socket(socket_blue).emit('message','your turn');
}
if (socket.id == socket_blue &&
socket_red != null &&
(status.length % 2 == 1)) {
status[status.length]=new move(col,row,"#0000FF");
io.sockets.socket(socket_red).emit('message','your turn');
io.sockets.socket(socket_blue).emit('message', 'waiting for red');
}
Client:
socket.on('message', function (data) {
p.innerHTML = data;
});
In the increments so far, each click as accepted. In the 4th increment, we check whether a click is OK. Also we inform the players whose turn it is.
Server:
- When a 'move' is received, a check is made (socket_blue != null) whether both players are already present. If not, the move is discarded.
- When a move is received, the length of the list of moves is checked. When odd, it's the blue players turn. When even, it's the red players turn: status.length % 2 == 0
- After the red player has played a move, the red player gets the message 'waiting for blue', and the blue player gets the message ' your turn'. And vice versa.
socket.on('move', function (col,row) {
if (socket.id == socket_red &&
socket_blue != null &&
(status.length % 2 == 0)) {
status[status.length]=new move(col,row,"#FF0000");
io.sockets.socket(socket_red).emit('message',
'waiting for blue');
io.sockets.socket(socket_blue).emit('message','your turn');
}
if (socket.id == socket_blue &&
socket_red != null &&
(status.length % 2 == 1)) {
status[status.length]=new move(col,row,"#0000FF");
io.sockets.socket(socket_red).emit('message','your turn');
io.sockets.socket(socket_blue).emit('message', 'waiting for red');
}
Client:
- Receives messages, and displays these in paragraph_1.
socket.on('message', function (data) {
p.innerHTML = data;
});
Increment 5:
The game is getting shape. This increment makes some improvements.
Server:
io.sockets.on('connection', function (socket) {
if (socket_red == null) {
socket_red = socket.id;
io.sockets.socket(socket_red).emit('message', 'waiting for 2nd player');
} else {
if (socket_blue == null) {
socket_blue = socket.id;
io.sockets.socket(socket_red).emit('message','your turn');
io.sockets.socket(socket_blue).emit('message', 'waiting for red');
}
}
...
socket.on('disconnect', function() {
if (socket.id == socket_red || socket.id == socket_blue) {
io.sockets.emit('message','game over');
reset_game();
}
});
...
function reset_game() {
socket_red = null;
socket_blue = null;
status = new Array();
}
The game is getting shape. This increment makes some improvements.
Server:
- When a connection is accepted (with io.sockets.on('connection', function (socket) ), the new player and (if applicable) the already connected player get a message. E.g. 'waiting for 2nd player', or 'your turn'.
- When one the players disconnects (by closing the browser window), the game is ended / reset. socket.on('disconnect', function() is introduced. As well as function reset_game().
io.sockets.on('connection', function (socket) {
if (socket_red == null) {
socket_red = socket.id;
io.sockets.socket(socket_red).emit('message', 'waiting for 2nd player');
} else {
if (socket_blue == null) {
socket_blue = socket.id;
io.sockets.socket(socket_red).emit('message','your turn');
io.sockets.socket(socket_blue).emit('message', 'waiting for red');
}
}
...
socket.on('disconnect', function() {
if (socket.id == socket_red || socket.id == socket_blue) {
io.sockets.emit('message','game over');
reset_game();
}
});
...
function reset_game() {
socket_red = null;
socket_blue = null;
status = new Array();
}
Increment 6:
This increments makes implements an important function: the check for 5-in-a-row. The game must indicate when a player has won.
Server:
socket.on('move', function (col,row) {
......
io.sockets.emit('status', JSON.stringify(status));
if (five_in_row() == true) {
io.sockets.emit('message','game over');
reset_game();
}
});
function five_in_row() {
var field = new Array(10);
for (var i=0; i<10; i++) {
field[i] = new Array(10);
}
for (var row=0; row<10; row++) {
for (var col=0; col<10; col++) {
field[col][row]=0;
}
}
for (var i=0; i< status.length; i++) {
field[status[i].col][status[i].row] = status[i].color;
}
for (var row=0; row<10; row++) {
for (col =0; col <6; col++) {
if ((field[row][col] != 0) &&
(field[row][col+1] == field[row][col]) &&
(field[row][col+2] == field[row][col]) &&
(field[row][col+3] == field[row][col]) &&
(field[row][col+4] == field[row][col]))
{ return true; }
}
}
for (var col=0; col<10; col++) {
for (row =0; row <6; row++) {
if ((field[row][col] != 0) &&
(field[row+1][col] == field[row][col]) &&
(field[row+2][col] == field[row][col]) &&
(field[row+3][col] == field[row][col]) &&
(field[row+4][col] == field[row][col]))
{ return true; }
}
}
return false;
}
This increments makes implements an important function: the check for 5-in-a-row. The game must indicate when a player has won.
Server:
- When a 'move' is made, the function five_in_row() is called. If this function detects 5-in-a-row, the game is over. This is reported to the players, and the game is reset. So, it is ready for a new game with new players.
- The function five_in_row() makes an overview of entered dots sofa, and checks whether there are 5 in a row.
socket.on('move', function (col,row) {
......
io.sockets.emit('status', JSON.stringify(status));
if (five_in_row() == true) {
io.sockets.emit('message','game over');
reset_game();
}
});
function five_in_row() {
var field = new Array(10);
for (var i=0; i<10; i++) {
field[i] = new Array(10);
}
for (var row=0; row<10; row++) {
for (var col=0; col<10; col++) {
field[col][row]=0;
}
}
for (var i=0; i< status.length; i++) {
field[status[i].col][status[i].row] = status[i].color;
}
for (var row=0; row<10; row++) {
for (col =0; col <6; col++) {
if ((field[row][col] != 0) &&
(field[row][col+1] == field[row][col]) &&
(field[row][col+2] == field[row][col]) &&
(field[row][col+3] == field[row][col]) &&
(field[row][col+4] == field[row][col]))
{ return true; }
}
}
for (var col=0; col<10; col++) {
for (row =0; row <6; row++) {
if ((field[row][col] != 0) &&
(field[row+1][col] == field[row][col]) &&
(field[row+2][col] == field[row][col]) &&
(field[row+3][col] == field[row][col]) &&
(field[row+4][col] == field[row][col]))
{ return true; }
}
}
return false;
}
Increment 7:
When using incremental design, you decide after each increment how to continue: (1) will there be a next increment, or is the project good enough, and (2) if there is a next increment: which requirements to add.
In this case, the program is quit OK, but not complete yet. If you want to complete it, you can do the last increment(s) yourself:
When using incremental design, you decide after each increment how to continue: (1) will there be a next increment, or is the project good enough, and (2) if there is a next increment: which requirements to add.
In this case, the program is quit OK, but not complete yet. If you want to complete it, you can do the last increment(s) yourself:
- The routine five_in_row() must also check for 5-in-a-row diagonal.
- The routine socket.on('move', function (col,row) must check whether the move has been done in an empty.