Page 1 of 1

Programming a JS game - part 2 (implementation) Rate Topic: -----

#1 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3576
  • View blog
  • Posts: 10,442
  • Joined: 08-June 10

Posted 28 November 2012 - 05:12 AM

This is the second part of the game programming tutorial, where we will encounter significantly more Javascript code than in the last part. But on we go:

3. View - the HTML implementation

In the introduction I said that I will use a very simple approach in presenting a game. The most simple approach I can think of when I hear "3x3 grid" is a 3x3 table.
<table>
    <tr>
        <td></td>
        <td></td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td></td>
        <td></td>
    </tr>
    <tr>
        <td></td>
        <td></td>
        <td></td>
    </tr>
</table>


To make it look right I create such a table in a HTML file and apply CSS so that it looks right. I won’t go into detail which CSS to choose and why, because that’s not the objective of this tutorial.

Then I delete the table HTML code.

... wait ... what?

Yes. I delete the hardcoded table because I am creating a Javascript game. Should a user (agent) that does not run JS load the page, the user should not be presented with false information (there is a 3x3 grid, it says TTT, so I can play). That may be different if the HTML layout is too complex to be generated by JS. A simple table like that definitely isn’t. The hardcoded table is just for ease of finding out fitting CSS rules. And of course there is another reason, midways in the Model chapter I said I numbered the fields. That is to be done somewhere [6] and where if not in JS itself?

So our first method of the View object is creating a table. We can consider this a standard problem, so I’m not going into detail into HTML creation via JS.
var View = {
	create: function() {
		var cell, row, table = document.createElement("table");
		// some prerequisites for CSS
		table.cellSpacing = 0;
		table.border = 1;
		// create 9 table cells
		for (var i = 0; i < 9; i++) {
			// before each 3 cells, create a table row
			if (0 === (i % 3)) {
				row = table.insertRow(Math.floor(i/3));
			}
			// create table cell in current table row
			cell = row.insertCell(i % 3);
			// insert the field number
			cell.field = i+1;
		}
		// return the table
		return table;
	}
};


In the MVC theory the View is responsible for presenting (e.g. displaying) the data of the Model (game state) to the user. So far we have created a table, that can hold the data from the game (which player marked which field), but we have yet to implement that step. It will turn out as we discuss the Controller part, that this task is so simple in JS, that we can omit an extra functionality in the View object (no-one prevents you from using a different Pattern when that other Pattern better suits your needs. That’s why I switched from classic MVC to Passive View (see spoiler above)).

[6] - I am aware that putting the numbers into the ID attributes is ... tempting, but doing that will cause additional code in the Controller or View, where you have to access these numbers. Eventually, IDs must not start with a number!
Well, you could use letters, but that will restrict you to using strings exclusively (DOM attribute properties are always (!) strings). By using JS for the table creation, I can pass whatever and as many data types I like. On the other-hand-side IDs must be unique ... You see there is almost always more than one way to solve a problem.

5. Controller - user interaction

In MVC, the Controller is responsible of processing the user input/interaction and change the Model (resp. its data) accordingly.

When we think of user interaction, we think of events like clicking, dragging, typing, etc. To let us interact with such events, JS (developers) built the event notification system. That is, if an event occurs, an Event object is liberated and it will travel through the JS document in a certain way through all concerned HTML elements (cf. W3C event model). Now we’re given the opportunity and create a handling function tied to an HTML element that fires if the event passes the respective element. This event system already covers quite a lot of the technical needs of a Controller object. What we have to do is decide upon a sufficient event and when our handler is triggered, we need to write the logic that changes the Model’s data. So the gist of our Controller object is to create an event handling function that mediates through the course of the game.

What would the appropriate event be in our case? The most obvious one is a click event in one of the table cells. If that cell is empty, draw in it the symbol of the current user, otherwise throw an error (or just ignore the action). Defining a click event handler is easy, in JS we use the EventTarget.addEventListener() method for that. So we would need to call that method on each table cell. Well, do we really need to do that on each cell? (I mean, 9 cells is not much … but I’m lazy). Actually, we don’t need to. JS supplies us with a way to access the cell where the click happened without having to define the handler on that cell! The event object that JS creates for us contains a certain property target, which contains the downmost element in which the event happened (in this case: <td>). That means, no matter where we click, Event.target will return the downmost element of the click. That is, if we click anywhere in our table, we get the targeted table cell (maybe with some border-case exception, that we will take care of)! So we only need to attach the handler to the <table> object once, and we get each cell that was clicked! How do we get the table element to attach the handler to? We created it in the View’s create() method, so that is good and done.
// a simplified example

// create table
var table = View.create();
// add handler
table.addEventListener("click", controller_handler);
// attach table to the document
table_parent.appendChild(table);

// the Event object will be passed as first parameter
// to the handler function automatically
function controller_handler(evt)
{
    var cell = evt.target;
    // more controller code
}


Before I proceed to the important Controller code a word to the end of the game. Once the game is over, no more action should allow the Model to be changed. One way to achieve that is to simply remove the event listener (EventTarget.removeEventListener()). Another way is to kill the event before it reaches our handler function (yes, in JS there is the possibility to kill an event). That could be achieved by creating another event handler that takes action before our original handler. For that to work we need our original handler to be placed in the bubbling phase of event traversal (EventTarget.addEventListener(event, handler, false)), while the new handler goes into the capturing phase (EventTarget.addEventListener(event, handler, true)). There it can kill the event by calling the Event’s stopPropagation() method. Which way to choose is personal preference, but both have their pros and cons. I decided to go for the handler removal here.

Now for the real Controller code. What work does it have to do?
  • check if the field is already marked
  • if still available, draw the symbol of the current user in it
  • update the Model/the game’s state


How do we know, whether a cell is free? Obviously, it is free if it neither contains an X or an O or any other character. In short, if it is empty. by skimming through the DOM properties, we find that the characters inside the cell are available through innerHTML or textContent. Hence if the length of those property values is zero, the cell must be free.
Controller = {
	action: function(evt) {
		// creating a short-cut
		var td = evt.target;
		// the first condition checks for the emptyness
		// the second that we’re actually in a cell
		// (remember the border-cases mentioned?)
		// should we incidentally hit a border,
		// the event target would be <tr> or <table>
		if (td.textContent.length || td.tagName.toLowerCase() !== "td") {
			// you may or may not notify the user that something went wrong
		//	alert("Field is already occupied.");
			return false;
		},

		// more Controller code
	}
};


Drawing the symbol is pretty easy as well, all we have to do is get the current player and assign its symbol to the table cell. But that requires that the Controller has access to the Model. For that we use another Design Pattern, which is called the Observer Pattern [7]. In the Observer Pattern each of the objects contain a reference to the other so that messages/notifications can be exchanged via the appropriate methods. Two of those methods we have already written: Model.nextTurn() and the win test Model.isWinner(). All that we still need to do is pass the field number to the player object.
Controller = {
	// our model, one way to do it. we will later change that
	model: Model, 
	action: function(evt) {
		var td = evt.target;
		if (td.textContent.length || td.tagName.toLowerCase() !== "td") {
		//	alert("Field is already occupied.");
			return false;
		}
		// Model.isWinner() throws a string in the winning case, 
		// so we need to use try…catch
		try {
			var mdl = Controller.model;
			// assign symbol
			td.textContent = mdl.current_player.symbol;
			// push field number into current player’s list of owned fields
			mdl.current_player.fields.push(+td.field);
			// test if current player wins the game
			mdl.isWinner();
			// invoke next move.
			// skipped if previous method found a winner
			mdl.nextTurn();
		}
		// if there is a winner 
		catch (msg) {
			if (typeof msg == "string") {
				alert(msg); // congratulate
				Board.quit_game(); // remove event handler
				return null;
			}
			throw msg; // in case there is coming a system error
		}
	}
};


What is left now is piece it all together and add some initiating and clean-up code where necessary. First we will put the View into the Controller as the View does not really do much on its own (it is not uncommon to combine View and Controller, as there is at least one Controller per View, but in the end it depends on the case if that is a good idea).
// there will be some renaming here and there
var Controller = {
	// use as static method
	createView: function() { 
		var cell, row, table = document.createElement("table");
		table.cellSpacing = 0;
		table.border = 1;
		for (var i = 0; i < 9; i++) {
			if (0 === (i % 3)) {
				row = table.insertRow(Math.floor(i/3));
			}
			cell = row.insertCell(i % 3);
			cell.field = i+1;
		}
		return table;
	},
	initWith: function(game) {
		// initialize game, esp. the player objects
		game.init(); 
		// attach model to controller
		Controller.model = game;
		// create view/table
		Controller.view = Controller.createView();
		// add handler function to view/table
		Controller.view.addEventListener("click", Controller.action);
	},
	action: function(evt) {
		var td = evt.target;
		if (td.textContent.length || td.tagName.toLowerCase() !== "td") {
			return false;
		}
		try {
			// some short-cut variables, also for a little speed-up
			var game   = Controller.model;
			var player = game.current_player;
			td.textContent = player.symbol;
			player.fields.push(+td.field);
			game.hasWinner();
			game.nextTurn();
		}
		catch (msg) {
			if (typeof msg == "string") {
				alert(msg);
				Controller.quit_game();
				return null;
			}
			throw msg;
		}
	},
	quit_game: function() {
		Controller.view.removeEventListener("click", Controller.action);
	}
};


The Model object

var Model = {
	players: [
		{
			name: "Player 1",
			symbol: "X",
			fields: []
		},
		{
			name: "Player 2",
			symbol: "O",
			fields: []
		}
	],
	turn_no: 0,
	current_player: {},
	winLines: [
		[1,2,3], [4,5,6], [7,8,9],
		[1,4,7], [2,5,8], [3,6,9],
		[1,5,9], [3,5,7]
	],
	init: function() {
		Model.turn_no = 1;
		// reset player states (no field occupied)
		Model.players[0].fields = [];
		Model.players[1].fields = [];
		Model.current_player = Model.players[0];
	},
	nextTurn: function() {
		Model.current_player = Model.players[Model.turn_no % 2];
		Model.turn_no       += 1;
	},
	hasWinner: function() {
		var fields = Model.current_player.fields;
		for (var l = Model.winLines.length; l--;​) {
			if (3 === Model.winLines[l].intersect(fields).length) {
				throw (Model.current_player.name + " wins!");
			}
		}
	}
};


And of course the intersection method
Array.prototype.intersect = function (arr) {
	return this.filter(function(item) {
		return (-1 !== arr.indexOf(item));
	});
};


All that’s left now is to intiate all the code.
Controller.initWith(Model);
document.getElementById("tictactoe").appendChild( Controller.view );



[7] - strictly speaking, this is not necessary here since all objects in use are Singletons. But that will not always be the case so I demonstrate the use of the Observer Pattern here.


5. Tutorial Aftermath

But what if ... there were 3 (n) players on a 4x4 (m x m) field?

Code that’d need modification:
  • Model.winLines: complete rewrite depending on field size, may be delegated to the init() method
  • Model.init(): re-set using a loop
  • Model.nextTurn(): replace % 2 by % players.length
  • Model.hasWinner(): replace 3 by winLines[i].length
  • Controller.createView(): replace 3 (9) by m (m*m) (the field size)

As you see, most modifications apply to replacing hard-coded values. The logic itself (the Controller) remains untouched.

New code:
  • Creating player objects via constructor should get a serious consideration, may be delegated to init() method.


This demonstrates the flexibility and re-useability of object oriented code!



That’s it for today! As always, comments are welcome.

Dormi

This post has been edited by Dormilich: 13 December 2012 - 12:54 PM


Is This A Good Question/Topic? 3
  • +

Replies To: Programming a JS game - part 2 (implementation)

#2 Miroidan  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 5
  • Joined: 28-September 14

Posted 27 October 2014 - 08:08 PM

(I read the rules for replies/answers and it says nothing about gravedigging.)

What happened to these tutorials? Is it discontinued fully or are you there somewhere?
Was This Post Helpful? 0
  • +
  • -

#3 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3576
  • View blog
  • Posts: 10,442
  • Joined: 08-June 10

Posted 28 October 2014 - 02:15 AM

View PostMiroidan, on 28 October 2014 - 05:08 AM, said:

What happened to these tutorials?

uh, itís finished?
Was This Post Helpful? 0
  • +
  • -

#4 Miroidan  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 5
  • Joined: 28-September 14

Posted 29 October 2014 - 06:34 AM

View PostDormilich, on 28 October 2014 - 02:15 AM, said:

uh, itís finished?


My bad.
Was This Post Helpful? 0
  • +
  • -

#5 Dormilich  Icon User is offline

  • 痛覚残留
  • member icon

Reputation: 3576
  • View blog
  • Posts: 10,442
  • Joined: 08-June 10

Posted 29 October 2014 - 07:02 AM

although, if you have an idea what absolutely has to be added, Iím all ears.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1