Difference between revisions of "Javascript1"
(16 intermediate revisions by the same user not shown) | |||
Line 2: | Line 2: | ||
[[Category: Current Classes]] | [[Category: Current Classes]] | ||
[[Category: Classes]] | [[Category: Classes]] | ||
+ | |||
+ | ==Announcements== | ||
+ | <!--This course is officially over, but I will be extending it for a couple of weeks into January to include some advanced topics for those who are interested. | ||
+ | If you were in the intro class during Nov-Dec, or have some Javascript experience and are willing to jump into the middle of a story, please sign up for the Advanced Javascript course. | ||
+ | We'll begin Tues, Jan 7, at 11:00.--> | ||
+ | |||
+ | Minesweeper demo code begins [[#Game engine/model|here]]. | ||
+ | |||
+ | <!--Meanwhile, warm up with Exercises [[#Exercises (Set 1)|Set 1]] and [[#Exercises (Set 2)|Set 2]].--> | ||
==General Information== | ==General Information== | ||
− | This course is currently running November- | + | This course is currently running November 2013-January 2014, Tuesdays 11:00-1:00. |
− | Part 1 (November) will offer students a solid foundation in Javascript as a general-purpose programming language. Part 2 ( | + | Part 1 (November-December) will offer students a solid foundation in Javascript as a general-purpose programming language. Part 2 (January) will explore Javascript's integration in web browsers with HTML, CSS, and the Document Object Model (DOM). |
Instructor: Dan Bauer (email: dsbauer at gmail) | Instructor: Dan Bauer (email: dsbauer at gmail) | ||
+ | |||
+ | |||
==Course Outline== | ==Course Outline== | ||
Line 354: | Line 365: | ||
</pre></div></div> | </pre></div></div> | ||
+ | ====Loops==== | ||
Loops have: | Loops have: | ||
* some form of counter ("iterator"), a variable which changes with each cycle of the loop | * some form of counter ("iterator"), a variable which changes with each cycle of the loop | ||
Line 401: | Line 413: | ||
* ACTIONs can be any statements, including nested loops | * ACTIONs can be any statements, including nested loops | ||
+ | ====Functions==== | ||
Functions: | Functions: | ||
* are mini-programs, independent modules which can be reused in multiple contexts | * are mini-programs, independent modules which can be reused in multiple contexts | ||
Line 443: | Line 456: | ||
|} | |} | ||
− | ====Exercises==== | + | ====Exercises (Set 1)==== |
'''1)''' Write a function to calculate, for a group of N people where everyone shakes the hand of everyone else, how many total handshakes there are. | '''1)''' Write a function to calculate, for a group of N people where everyone shakes the hand of everyone else, how many total handshakes there are. | ||
Line 626: | Line 639: | ||
===Week 3: Objects, Arrays, and Methods=== | ===Week 3: Objects, Arrays, and Methods=== | ||
− | |||
====Objects==== | ====Objects==== | ||
Line 777: | Line 789: | ||
<ul> | <ul> | ||
<li> Numbers: | <li> Numbers: | ||
+ | <pre> | ||
var four= Object(4); //number wrapper object | var four= Object(4); //number wrapper object | ||
typeof four;//"object" | typeof four;//"object" | ||
Line 783: | Line 796: | ||
four.valueOf();//4 | four.valueOf();//4 | ||
four*3; //12 | four*3; //12 | ||
+ | </pre> | ||
<li> Strings: | <li> Strings: | ||
+ | <pre> | ||
var xyz= Object("x,y,z"); //string wrapper object | var xyz= Object("x,y,z"); //string wrapper object | ||
xyz== "x,y,z";//true | xyz== "x,y,z";//true | ||
Line 790: | Line 805: | ||
xyz[2];//"y" | xyz[2];//"y" | ||
xyz.split(",");//array ["x","y","z"] | xyz.split(",");//array ["x","y","z"] | ||
+ | </pre> | ||
+ | String primitives auto-convert to wrapper objects: | ||
+ | <pre> | ||
+ | "a b c".split(" ");//array ["a","b","c"] | ||
+ | "a b c"[4];//"b" | ||
+ | </pre> | ||
+ | </ul> | ||
+ | |||
+ | |||
+ | ====Exercises (Set 2)==== | ||
+ | '''4)''' Revisit Exercise #1b. Build a data structure which represents all people and their meetings... | ||
+ | |||
+ | '''a)''' Assume there are 5 people. Give them each a name. Create an array called ''people'' of length 5 holding their names. | ||
+ | For these exercises, you may use a global variable for ''people'', but recognize that's a dangerous practice in real programming! | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | var people=['Alan','Betty','Carl','Dana','Emma']; | ||
+ | </div></div> | ||
+ | |||
+ | '''b)''' Rewrite your pair-enumeration function to use your ''people'' array. | ||
+ | <!--Change the function's parameter to receive the array instead of a number...--> | ||
+ | Use the array size to simulate all meetings and substitute people's names for numbers, as in: | ||
+ | "Alan meets Betty. Alan meets Carl. Betty meets Carl..." | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | function writePair(personA,personB) { | ||
+ | return people[personA]+" meets "+people[personB]+". "; | ||
+ | } | ||
+ | |||
+ | function enumeratePairs() { | ||
+ | var result=""; | ||
+ | for (var personA=0; personA<(people.length-1); personA++) { | ||
+ | for (var personB=personA+1; personB<people.length; personB++) { | ||
+ | result+=writePair(personA,personB); | ||
+ | } | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | '''c)''' Instead of constructing your ''people'' array manually, write a function to do it incrementally. Function ''addPerson(name)'' should add ''name'' to the array each time it's called. | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | var people=[]; | ||
+ | function addPerson(name) { | ||
+ | people.push(name); | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | '''d)''' Now represent each person with an object. Each object should have a ''name'' property. | ||
+ | Modify your ''addPerson'' function to fill the ''people'' array with objects instead of name strings. Change your enumeration function as needed to work with the object array. | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | function addPerson(name) { | ||
+ | people.push({name:name}); | ||
+ | } | ||
+ | function writePair(indexA,indexB) { | ||
+ | // retrieve objects by index: | ||
+ | var personA=people[indexA]; | ||
+ | var personB=people[indexB]; | ||
+ | return personA.name+" meets "+personB.name+". "; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | <!-- | ||
+ | e) Change the ''people'' array so that each person-object can be referenced by name as well as by number. First, make sure each person has a unique name. Next, whenever a new person is added to the array, add a property to ''people'' with the same name as the person, and set its value to be a copy of the same person-object which is indexed by number. | ||
+ | For example, if Carl is the third person, both ''people[2]'' and ''people.Carl'' should refer to the same object, whose name is ''Carl''. | ||
+ | --> | ||
+ | '''e)''' Make your data structure remember all the meetings. To each object representing a person P in the ''people'' array, add a property ''friends'' which stores an array of the names of all people whom P has met. | ||
+ | Change your enumerate function to add names to these lists with each meeting. | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | function addPerson(name) { | ||
+ | people.push({name:name, friends:[]}); | ||
+ | } | ||
+ | function writePair(indexA,indexB) { | ||
+ | //retrieve objects: | ||
+ | var personA=people[indexA]; | ||
+ | var personB=people[indexB]; | ||
+ | personA.friends.push(personB.name); | ||
+ | personB.friends.push(personA.name); | ||
+ | return personA.name+" meets "+personB.name+". "; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | '''f)''' Change the ''friends'' array of each person to store not the names but the objects of other people. Remember that an object can have multiple references. You need to have only one object per person, but with references to it in both ''people'' and multiple ''friends'' arrays. | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | function writePair(indexA,indexB) { | ||
+ | //retrieve objects: | ||
+ | var personA=people[indexA]; | ||
+ | var personB=people[indexB]; | ||
+ | personA.friends.push(personB); | ||
+ | personB.friends.push(personA); | ||
+ | return personA.name+" meets "+personB.name+". "; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | '''g)''' Once all meetings have been recorded and all ''friends'' lists built, write a function ''newestFriendOf(personNum)'' which returns the name of the last person met by person # ''personNum''. | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | function newestFriendOf(personNum) { | ||
+ | var person = people[personNum]; | ||
+ | var friend = person.friends[person.friends.length-1]; | ||
+ | return friend.name; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | '''h)''' Turn all of your functions into methods of the ''people'' array. When a function needs to refer to the ''people'' array, use the keyword ''this'' instead. | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Solution...''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | var people=[]; | ||
+ | |||
+ | people.addPerson = function(name) { | ||
+ | this.push({name:name, friends:[]}); | ||
+ | } | ||
+ | |||
+ | people.writePair = function(indexA,indexB) { | ||
+ | //retrieve objects: | ||
+ | var personA=this[indexA]; | ||
+ | var personB=this[indexB]; | ||
+ | personA.friends.push(personB); | ||
+ | personB.friends.push(personA); | ||
+ | return personA.name+" meets "+personB.name+". "; | ||
+ | } | ||
+ | |||
+ | people.enumeratePairs = function() { | ||
+ | var result=""; | ||
+ | for (var personA=0; personA<(this.length-1); personA++) { | ||
+ | for (var personB=personA+1; personB<this.length; personB++) { | ||
+ | result+=this.writePair(personA,personB); | ||
+ | } | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | |||
+ | people.newestFriendOf = function(personNum) { | ||
+ | var person = this[personNum]; | ||
+ | var friend = person.friends[person.friends.length-1]; | ||
+ | return friend.name; | ||
+ | } | ||
+ | </div></div> | ||
+ | |||
+ | |||
+ | <!-- | ||
+ | Create an array of length N listing, for each person, the names of the other people met that night. | ||
+ | |||
+ | c) Represent each person as an object with two properties: ''name'' and ''friends''. | ||
+ | Friends should be an array, initially empty. | ||
+ | As people meet, each ''friends'' array will fill up with references to the objects of other people. | ||
− | + | d) Rewrite your enumeration function to fill up all ''friends'' arrays. | |
− | + | ||
+ | e) Write a function getFriends | ||
+ | |||
+ | |||
+ | c) Combine your arrays from a) and b) into a single array which | ||
+ | --> | ||
+ | |||
+ | <!-- Too difficult... | ||
+ | '''5)''' Improve your prime-number check from Exercise #2. Exploit the fact that if a number is divisible by a non-prime (e.g. 9), it's also divisible by a prime (e.g. 3). Therefore it's sufficient to test only prime factors. | ||
+ | |||
+ | Write two function which both use an array listing all known primes in increasing order: | ||
+ | * array knownPrimes will list all known primes in increasing order, i.e. [2,3,5,7...]. | ||
+ | It will start out empty and grow as needed. | ||
+ | * function findNextPrime(after) should find the first prime number greater than ''after'', add it to your array, and return it. | ||
+ | * function | ||
+ | --> | ||
+ | |||
+ | ===Week 4: Practice with Objects, Arrays, and Functions=== | ||
+ | This week is a break from new content, instead offering more practice with real code using Objects, Arrays, and Functions. | ||
====Demo Code==== | ====Demo Code==== | ||
+ | These code samples begin with a portion of Exercise #3 (modelling playing cards) and improve the design incrementally in successive versions. | ||
+ | You should understand the differences between versions and the rationale for each change. | ||
<div class="toccolours mw-collapsible mw-collapsed"> | <div class="toccolours mw-collapsible mw-collapsed"> | ||
Line 986: | Line 1,184: | ||
</div></div> | </div></div> | ||
+ | <!-- | ||
+ | ===Week 6: Prototypes, Inheritance, and Subclassing=== | ||
+ | ====Prototypes==== | ||
+ | |||
+ | ====Exercises Part 3==== | ||
+ | |||
+ | '''7)''' Now that you have a well-developed representation of a single playing card, write the | ||
+ | --> | ||
<!-- | <!-- | ||
====Exercises==== | ====Exercises==== | ||
Line 993: | Line 1,199: | ||
--> | --> | ||
− | ===Week 4: Constructors, Prototypes, and | + | ===Week 5: Constructors, Prototypes, and Inheritance=== |
+ | |||
+ | ====Constructors==== | ||
+ | A constructor is simply a function whose purpose is to initialize an object (usually empty) | ||
+ | to behave as an instance of the "class" the constructor represents. There are no "class" objects in JS, but constructors are the closest approximation. | ||
+ | |||
+ | Think of a constructor as a temporary method which is attached to an object only for one execution, during which it creates and sets the appropriate object properties. | ||
+ | |||
+ | A constructor has: | ||
+ | * no creation of its intended object (done beforehand by the ''new'' operator); | ||
+ | * no return value; | ||
+ | * the keyword ''this'', referring to its temporary "owner" object; | ||
+ | * by convention, a name which is a noun phrase starting with capital letter (e.g. Card, BananaCremePie, PresidentsOfMars) | ||
+ | |||
+ | Some predefined constructors: | ||
+ | * ''Object'' | ||
+ | * ''Array'' | ||
+ | * ''Function'' | ||
+ | * ''Number'' | ||
+ | * ''String'' | ||
+ | |||
+ | ====The ''new'' operator==== | ||
+ | Constructors are normally called with a special operator, ''new'', which creates an empty object and applies the constructor to it. | ||
+ | The expression ''new CTOR(args)'' does (approximately) the following steps: | ||
+ | # create an empty object OBJ | ||
+ | # link OBJ to CTOR's prototype (see [[#Prototypes]]) | ||
+ | # temporarily reinterpret the word ''this'' to refer to OBJ | ||
+ | # executes CTOR(args), which initializes OBJ using ''this'' | ||
+ | # restore the previous meaning of ''this'' | ||
+ | # return OBJ as the expression's value | ||
+ | |||
+ | Using predefined constructors: | ||
+ | var obj; | ||
+ | obj = new Object(); //--> return empty Object, i.e. {} | ||
+ | obj = new Array(); //-->empty Array, i.e. [] | ||
+ | obj = new Array(1,2,3); //--> initialized Array [1,2,3] | ||
+ | obj = new Array(3); //--> Array of 3 undefineds: [undefined,undefined,undefined], NOT [3] | ||
+ | obj = new Function('x','y','return x+y;');//--> function(x,y) {return x+y;} | ||
+ | obj = new String("hello"); //wrapper obj containing "hello", NOT primitive string "hello" | ||
+ | obj = new Number(12); //wrapper obj containing 12, NOT primitive number 12 | ||
+ | |||
+ | |||
+ | ====Constructors without ''new''==== | ||
+ | If ''new'' is omitted, some predefined constructors return primitives: | ||
+ | obj = String("hello");// primitive string "hello" | ||
+ | obj = Number(12);// primitive number 12 | ||
+ | Others return objects: | ||
+ | obj = Object(5);//wrapper obj containing 5 | ||
+ | obj = Array(1,2,3); //Array object [1,2,3] | ||
+ | But such behavior is atypical; in general, you should always use ''new'' with constructors! | ||
+ | |||
+ | Except for special cases, calling a constructor without using ''new'' means that its reference ''this'' points to the wrong object! Usually it points to the global object, so that constructors will wrongly create global properties: | ||
+ | function BlueSquare() {this.color="blue"; this.sides=4;} //a constructor | ||
+ | obj = new BlueSquare();// CORRECT, returns object: {color:"blue", sides:4} | ||
+ | obj = BlueSquare(); // WRONG... | ||
+ | obj; //--> undefined, function has no return val | ||
+ | color; //--> "blue" | ||
+ | sides; //--> 4 | ||
+ | |||
+ | ====Constructors as Classes==== | ||
+ | Constructors unify their instances (the objects they initialize) into a family or "class": | ||
+ | * All instances have a built-in ''constructor'' property which remembers the constructor which made them. It can be a convenient way to check the subtype of an object: | ||
+ | obj = new Card(); | ||
+ | obj.constructor == Card; //true | ||
+ | * Alternatively, the operator ''instanceof'' returns true for certain combinations of instance and constructor: | ||
+ | obj instanceof Card; //true | ||
+ | obj instanceof Object; //true (everything is an Object) | ||
+ | Card instanceof Object; //true | ||
+ | Function instanceof Object; //true | ||
+ | Card instanceof Function; //true (every Ctor is a Function) | ||
+ | Object instanceof Function; //true | ||
+ | Function instanceof Function; //true | ||
+ | * Properties added to a constructor can store resources (e.g. Arrays) shared by all its instances (as can [[#Prototypes]]); | ||
+ | * Methods added to a constructor can compute class information without needing any particular instance. For example: | ||
+ | function Tower(hgt) { //constructor makes towers, remembers highest | ||
+ | this.height=hgt; | ||
+ | if (hgt > Tower.highestEver()) { | ||
+ | Tower.highestEver = function() {return hgt;} //revise definition | ||
+ | } | ||
+ | } | ||
+ | Tower.highestEver = function() {return 0;} //initial definition | ||
+ | |||
+ | |||
+ | ====Demo code, part 2==== | ||
+ | Continuing improvements to cards demo... | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Version 5: Card constructor''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | //===== Version 5: Card constructor | ||
+ | <br> | ||
+ | // makeCard (optional) is a wrapper which calls Card constructor | ||
+ | function makeCard(cardnum) { | ||
+ | return new Card(cardnum); | ||
+ | } | ||
+ | <br> | ||
+ | // Constructor: | ||
+ | function Card(cardnum) { | ||
+ | this.rank = Math.floor(cardnum/4)+1; | ||
+ | this.suit = cardnum%4+1; | ||
+ | <br> | ||
+ | this.color = function() { | ||
+ | return (this.suit<3)? "red":"black"; | ||
+ | } | ||
+ | this.name = function() { | ||
+ | return (Card.rankNames[this.rank] | ||
+ | +" of " | ||
+ | +Card.suitNames[this.suit]); | ||
+ | } | ||
+ | //no return needed | ||
+ | } | ||
+ | <br> | ||
+ | Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; | ||
+ | Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"]; | ||
+ | |||
+ | //Testing: | ||
+ | var card51 = new Card(51); | ||
+ | card51.color(); //black | ||
+ | card51.name(); //King of Clubs | ||
+ | </div></div> | ||
+ | |||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Version 6: Card constructor w. shared prototype methods''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | //===== Version 6: Card constructor + prototype | ||
+ | |||
+ | function makeCard(cardnum) { | ||
+ | return new Card(cardnum); | ||
+ | } | ||
+ | |||
+ | // Constructor: | ||
+ | function Card(cardnum) { | ||
+ | this.rank = Math.floor(cardnum/4)+1; | ||
+ | this.suit = cardnum%4+1; | ||
+ | } | ||
+ | |||
+ | // Shared methods: | ||
+ | Card.prototype.name = function() { | ||
+ | return (Card.rankNames[this.rank] | ||
+ | +" of " | ||
+ | +Card.suitNames[this.suit]); | ||
+ | } | ||
+ | Card.prototype.color = function() { | ||
+ | return (this.suit<3)? "red":"black"; | ||
+ | } | ||
+ | |||
+ | |||
+ | Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; | ||
+ | Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"]; | ||
+ | |||
+ | |||
+ | //Testing: | ||
+ | var card51 = new Card(51); | ||
+ | card51.name(); // --> King of Clubs | ||
+ | </pre></div></div> | ||
+ | |||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Version 7: Card subclassing, inheritance''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | //==== Version 7: Card subclassing | ||
+ | |||
+ | // Superclass constructor: | ||
+ | function Card(cardnum) { | ||
+ | this.rank = Math.floor(cardnum/4)+1; | ||
+ | this.suit = cardnum%4+1; | ||
+ | } | ||
+ | |||
+ | Card.prototype.name = function() { | ||
+ | return (this.constructor.rankNames[this.rank] | ||
+ | +" of " | ||
+ | +this.constructor.suitNames[this.suit]); | ||
+ | } | ||
+ | Card.prototype.color = function() { | ||
+ | return (this.suit<3)? "red":"black"; | ||
+ | } | ||
+ | |||
+ | Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; | ||
+ | Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"]; | ||
+ | |||
+ | |||
+ | // Subclass constructor: (Minor Arcana of Tarot deck) | ||
+ | function MinorArcana(cardnum,orientation) { | ||
+ | Card.call(this,cardnum); //initialize as superclass (Card) | ||
+ | //add subclass-specific properties: | ||
+ | this.orientation = orientation; | ||
+ | } | ||
+ | |||
+ | MinorArcana.rankNames = Card.rankNames.slice(0); //copy all | ||
+ | MinorArcana.rankNames.splice(11,1,"Page","Knight");//replace J w. P+K | ||
+ | MinorArcana.suitNames = ["","Cups","Coins","Swords","Wands"]; | ||
+ | |||
+ | MinorArcana.prototype = new Card(); | ||
+ | MinorArcana.prototype.constructor = MinorArcana; | ||
+ | |||
+ | //Testing: | ||
+ | var normal42 = new Card(42); | ||
+ | normal42.name(); // --> Jack of Spades | ||
+ | var tarot42 = new MinorArcana(42,"upright"); | ||
+ | tarot42.name(); // --> Page of Swords | ||
+ | tarot42.orientation; //--> upright | ||
+ | </pre></div></div> | ||
+ | |||
+ | ====The ''call'' method==== | ||
+ | Some explanation is needed for this mysterious line in Version 7: | ||
+ | Card.call(this,cardnum) | ||
+ | |||
+ | Every function has a predefined method (i.e. a member function) named ''call'' which executes the function, similarly to the call operator ''_(_)''. | ||
+ | The difference is that a function's ''call'' method expects one additional argument, an object, in the first position, and it calls its owner function as if that function were a method of that object. | ||
+ | For example: | ||
+ | var dot = {radius:1, color:"white"}; | ||
+ | function paint(newcolor) { | ||
+ | this.color = newcolor; | ||
+ | } | ||
+ | paint.call(dot,"blue");//calls paint as if it belongs to dot (i.e. this==dot) | ||
+ | dot.color;//--> "blue" | ||
+ | |||
+ | |||
+ | This "temporary ownership" can be used to apply a constructor manually (without ''new'') to initialize an existing object. | ||
+ | In the demo line | ||
+ | Card.call(this,cardnum) | ||
+ | a subclass of Card is using Card as an initializer in the first step of its own initialization. | ||
<!--==Sample Code== | <!--==Sample Code== | ||
[[Media:JS1-Nov12-scratchpad | Nov12 in-class scratchpad]] | [[Media:JS1-Nov12-scratchpad | Nov12 in-class scratchpad]] | ||
--> | --> | ||
+ | ==Advanced Topic: Building a simple application (Minesweeper)== | ||
+ | === Overview === | ||
+ | We're building a simple version of the game Minesweeper. | ||
+ | Like many applications, this one will separate the roles of Model and View into two modules: | ||
+ | * the game engine (Model) will manage the abstract structure and rules of the game; | ||
+ | * the interface (View) will control the display and user input. If time permits, we'll write more than one interface module to illustrate that the same Model works with both. | ||
+ | ==== Outer HTML file ==== | ||
+ | The modules are combined into a single application by including the javascript (and any CSS) for each in a single HTML file. | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''minesweeper.html''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | <html> | ||
+ | <head> | ||
+ | <meta charset="utf-8"> | ||
+ | <script type="text/javascript" src="mines7.js"></script> | ||
+ | <script type="text/javascript" src="minesweeper-gui.js"></script> | ||
+ | <link type="text/css" rel="StyleSheet" href="minesweeper-gui.css"> | ||
+ | </head> | ||
+ | <body onload="makeMinefield(10,10,15);"> | ||
+ | <div id="gamespace"></div> | ||
+ | </body> | ||
+ | </html> | ||
+ | </pre></div></div> | ||
+ | |||
+ | This file is just a preview; it won't work until we've finished the components. | ||
+ | |||
+ | === Game engine/model === | ||
+ | ==== Version 1: Basic grid geometry ==== | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''mines1.js''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | // Stage 1: Grid and Cell objects + basic coordinate system | ||
+ | //--- Grid --- | ||
+ | function Grid(w,h,bombs) { | ||
+ | this.length = w*h; | ||
+ | this.w = w; | ||
+ | this.h = h; | ||
+ | |||
+ | for (var n=0; n<this.length; n++) { | ||
+ | this[n] = new Cell(n,this); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | Grid.prototype.XYtoN = function(x,y) { | ||
+ | return (y*this.w)+x;//-->n | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoX = function(n) { | ||
+ | return n % this.w;//-->x | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoY = function(n) { | ||
+ | return Math.floor(n/this.w);//-->y | ||
+ | } | ||
+ | |||
+ | |||
+ | //--- Cell --- | ||
+ | function Cell(n,grid) { | ||
+ | this.grid = grid; | ||
+ | this.n = n; | ||
+ | this.x = grid.NtoX(n); | ||
+ | this.y = grid.NtoY(n); | ||
+ | } | ||
+ | |||
+ | Cell.prototype.neighbors = function() { //returns list of grid positions | ||
+ | // four booleans (true if NOT adjacent to respective grid edge): | ||
+ | var goW = (this.x>0); | ||
+ | var goE = (this.x+1 < this.grid.w); | ||
+ | var goN = (this.y>0); | ||
+ | var goS = (this.y+1 < this.grid.h); | ||
+ | |||
+ | var result=[]; | ||
+ | if (goN) { | ||
+ | var northN = this.n-this.grid.w; | ||
+ | if (goW) result.push(northN-1);//NW | ||
+ | result.push(northN);//N | ||
+ | if (goE) result.push(northN+1);//NE | ||
+ | } | ||
+ | if (goW) result.push(this.n-1);//W | ||
+ | if (goE) result.push(this.n+1);//E | ||
+ | if (goS) { | ||
+ | var southN =this.n+this.grid.w; | ||
+ | if (goW) result.push(southN-1);//SW | ||
+ | result.push(southN);//S | ||
+ | if (goE) result.push(southN+1);//SE | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | |||
+ | // Start: | ||
+ | function makeMinefield(w,h,bombs) { | ||
+ | window.minefield= | ||
+ | new Grid(w,h,bombs); | ||
+ | } | ||
+ | </pre></div></div> | ||
+ | |||
+ | ===== Related Exercise ===== | ||
+ | Implement a similar coordinate system in 3D--- specifically, for a Rubik's Cube. | ||
+ | Imagine that each of the 27 smaller cubes (include the center) in a Rubik's Cube is numbered in two different ways: | ||
+ | * each has a unique number N, from 0 to 26, and | ||
+ | * each has a unique three-component coordinate (X,Y,Z), each component from 0 to 2, describing its position in the big cube. | ||
+ | Write four functions to convert between these two systems: | ||
+ | * function XYZtoN(x,y,z) should convert (X,Y,Z) to N | ||
+ | * functions NtoX(n), NtoY(n), and NtoZ(n) should each convert N to one component of (X,Y,Z) | ||
+ | |||
+ | There are many solutions! | ||
+ | |||
+ | ==== Version 6: Full Game ==== | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''mines6.js''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | // Stage 1: basic coordinate system | ||
+ | // Stage 2: add mines,count | ||
+ | // Stage 3: add visibility,reveal | ||
+ | // Stage 4: add flags | ||
+ | // Stage 5: add win/lose | ||
+ | // Stage 6: attach UI! | ||
+ | |||
+ | if (typeof gui == "undefined") | ||
+ | var gui = null; | ||
+ | |||
+ | //--- Grid --- | ||
+ | function Grid(w,h,bombs) { | ||
+ | this.length = w*h; | ||
+ | this.w = w; | ||
+ | this.h = h; | ||
+ | this.numVisible = 0; | ||
+ | |||
+ | if (gui) this.token = gui.makeGrid(this,w,h); | ||
+ | |||
+ | for (var n=0; n<this.length; n++) { | ||
+ | this[n] = new Cell(n,this); | ||
+ | } | ||
+ | |||
+ | //limit bomb density to <1/4 | ||
+ | var maxbombs = Math.floor(this.length/4); | ||
+ | if (bombs>maxbombs) bombs=maxbombs; | ||
+ | |||
+ | this.numBombs = bombs; | ||
+ | this.flagsAvail = bombs; | ||
+ | while (bombs) { | ||
+ | n = Math.floor(Math.random()*this.length); | ||
+ | var cell = this[n]; | ||
+ | if (!cell.isBomb) { | ||
+ | cell.placeBomb(); | ||
+ | bombs--; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Grid.prototype.XYtoN = function(x,y) { | ||
+ | return (y*this.w)+x;//-->n | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoX = function(n) { | ||
+ | return n % this.w;//-->x | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoY = function(n) { | ||
+ | return Math.floor(n/this.w);//-->y | ||
+ | } | ||
+ | |||
+ | Grid.prototype.reveal = function(x,y) { | ||
+ | var n=this.XYtoN(x,y); | ||
+ | this[n].reveal(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.revealAll = function() { | ||
+ | for (var n=0; n<this.length; n++) { | ||
+ | this[n].reveal(true); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | Grid.prototype.flag = function(x,y) { | ||
+ | var n=this.XYtoN(x,y); | ||
+ | this[n].flag(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.checkWin = function() { | ||
+ | if (this.flagsAvail==0 && (this.numVisible>= this.length-this.numBombs)) | ||
+ | this.youWin(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.youWin = function() { | ||
+ | alert("You win!"); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.youLose = function() { | ||
+ | alert("KABOOM!"); | ||
+ | this.revealAll(); | ||
+ | } | ||
+ | |||
+ | //--- Cell --- | ||
+ | function Cell(n,grid) { | ||
+ | this.grid = grid; | ||
+ | this.n = n; | ||
+ | this.x = grid.NtoX(n); | ||
+ | this.y = grid.NtoY(n); | ||
+ | this.count = 0; | ||
+ | this.isBomb = false; | ||
+ | this.isVisible = false; | ||
+ | this.isFlag = false; | ||
+ | |||
+ | if (gui) this.token=gui.makeCell(grid.token,this,this.x,this.y); | ||
+ | } | ||
+ | |||
+ | Cell.prototype.neighbors = function() { //returns list of grid positions | ||
+ | // four booleans (true if NOT adjacent to respective grid edge): | ||
+ | var goW = (this.x>0); | ||
+ | var goE = (this.x+1 < this.grid.w); | ||
+ | var goN = (this.y>0); | ||
+ | var goS = (this.y+1 < this.grid.h); | ||
+ | |||
+ | var result=[]; | ||
+ | if (goN) { | ||
+ | var northN = this.n-this.grid.w; | ||
+ | if (goW) result.push(northN-1);//NW | ||
+ | result.push(northN);//N | ||
+ | if (goE) result.push(northN+1);//NE | ||
+ | } | ||
+ | if (goW) result.push(this.n-1);//W | ||
+ | if (goE) result.push(this.n+1);//E | ||
+ | if (goS) { | ||
+ | var southN =this.n+this.grid.w; | ||
+ | if (goW) result.push(southN-1);//SW | ||
+ | result.push(southN);//S | ||
+ | if (goE) result.push(southN+1);//SE | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | |||
+ | Cell.prototype.placeBomb = function() { | ||
+ | this.isBomb = true; | ||
+ | var neighbors = this.neighbors(); | ||
+ | for (var i=0; i<neighbors.length; i++) | ||
+ | this.grid[neighbors[i]].count++; | ||
+ | } | ||
+ | |||
+ | Cell.prototype.reveal = function(skipCheck) { | ||
+ | if (this.isVisible || this.isFlag) return; | ||
+ | this.isVisible = true; | ||
+ | this.grid.numVisible++; | ||
+ | if (gui) gui.reveal(this.token,this.count,this.isBomb); | ||
+ | |||
+ | if (!skipCheck) { | ||
+ | if (this.isBomb) { | ||
+ | this.grid.flagsAvail--; | ||
+ | this.grid.youLose(); | ||
+ | return; | ||
+ | } | ||
+ | this.grid.checkWin(); | ||
+ | } | ||
+ | |||
+ | if (this.count==0) { | ||
+ | var neighbors = this.neighbors(); | ||
+ | for (var i=0; i<neighbors.length; i++) | ||
+ | this.grid[neighbors[i]].reveal(); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Cell.prototype.flag = function() { | ||
+ | if (this.isVisible) return; | ||
+ | //toggle on or off: | ||
+ | if (!this.isFlag) {//no flag | ||
+ | if (this.grid.flagsAvail==0) return;//none available | ||
+ | this.grid.flagsAvail--; | ||
+ | } else {//has flag, remove it | ||
+ | this.grid.flagsAvail++; | ||
+ | } | ||
+ | this.isFlag = !this.isFlag; | ||
+ | |||
+ | if (gui) gui.flag(this.token,this.isFlag); | ||
+ | this.grid.checkWin(); | ||
+ | } | ||
+ | |||
+ | // Start: | ||
+ | function makeMinefield(w,h,bombs) { | ||
+ | window.minefield= | ||
+ | new Grid(w,h,bombs); | ||
+ | } | ||
+ | </pre></div></div> | ||
+ | |||
+ | ==== Version 7: Bonus Variant: Wrap-around grid ==== | ||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''mines7.js''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | // Stage 1: basic coordinate system | ||
+ | // Stage 2: add mines,count | ||
+ | // Stage 3: add visibility,reveal | ||
+ | // Stage 4: add flags | ||
+ | // Stage 5: add win/lose | ||
+ | // Stage 6: attach UI! | ||
+ | // Stage 7: subclass Cell for wrap-around | ||
+ | |||
+ | if (typeof gui == "undefined") | ||
+ | var gui = null; | ||
+ | |||
+ | //--- Grid --- | ||
+ | function Grid(celltype,w,h,bombs) { | ||
+ | this.length = w*h; | ||
+ | this.w = w; | ||
+ | this.h = h; | ||
+ | this.numVisible = 0; | ||
+ | |||
+ | if (gui) this.token = gui.makeGrid(this,w,h); | ||
+ | |||
+ | for (var n=0; n<this.length; n++) { | ||
+ | this[n] = new celltype(n,this); | ||
+ | } | ||
+ | |||
+ | //limit bomb density to <1/4 | ||
+ | var maxbombs = Math.floor(this.length/4); | ||
+ | if (bombs>maxbombs) bombs=maxbombs; | ||
+ | |||
+ | this.numBombs = bombs; | ||
+ | this.flagsAvail = bombs; | ||
+ | while (bombs) { | ||
+ | n = Math.floor(Math.random()*this.length); | ||
+ | var cell = this[n]; | ||
+ | if (!cell.isBomb) { | ||
+ | cell.placeBomb(); | ||
+ | bombs--; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Grid.prototype.XYtoN = function(x,y) { | ||
+ | return (y*this.w)+x;//-->n | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoX = function(n) { | ||
+ | return n % this.w;//-->x | ||
+ | } | ||
+ | |||
+ | Grid.prototype.NtoY = function(n) { | ||
+ | return Math.floor(n/this.w);//-->y | ||
+ | } | ||
+ | |||
+ | Grid.prototype.reveal = function(x,y) { | ||
+ | var n=this.XYtoN(x,y); | ||
+ | this[n].reveal(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.revealAll = function() { | ||
+ | for (var n=0; n<this.length; n++) { | ||
+ | this[n].reveal(true); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | Grid.prototype.flag = function(x,y) { | ||
+ | var n=this.XYtoN(x,y); | ||
+ | this[n].flag(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.checkWin = function() { | ||
+ | if (this.flagsAvail==0 && (this.numVisible>= this.length-this.numBombs)) | ||
+ | this.youWin(); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.youWin = function() { | ||
+ | alert("You win!"); | ||
+ | } | ||
+ | |||
+ | Grid.prototype.youLose = function() { | ||
+ | alert("KABOOM!"); | ||
+ | this.revealAll(); | ||
+ | } | ||
+ | |||
+ | //--- Cell --- | ||
+ | function Cell(n,grid) { | ||
+ | if (!grid) return; | ||
+ | this.grid = grid; | ||
+ | this.n = n; | ||
+ | this.x = grid.NtoX(n); | ||
+ | this.y = grid.NtoY(n); | ||
+ | // this.count = 0; | ||
+ | this.isBomb = false; | ||
+ | this.isVisible = false; | ||
+ | this.isFlag = false; | ||
+ | |||
+ | if (gui) this.token=gui.makeCell(grid.token,this,this.x,this.y); | ||
+ | } | ||
+ | |||
+ | Cell.prototype.neighbors = function() { //returns list of grid positions | ||
+ | // four booleans (true if NOT adjacent to respective grid edge): | ||
+ | var goW = (this.x>0); | ||
+ | var goE = (this.x+1 < this.grid.w); | ||
+ | var goN = (this.y>0); | ||
+ | var goS = (this.y+1 < this.grid.h); | ||
+ | |||
+ | var result=[]; | ||
+ | if (goN) { | ||
+ | var northN = this.n-this.grid.w; | ||
+ | if (goW) result.push(northN-1);//NW | ||
+ | result.push(northN);//N | ||
+ | if (goE) result.push(northN+1);//NE | ||
+ | } | ||
+ | if (goW) result.push(this.n-1);//W | ||
+ | if (goE) result.push(this.n+1);//E | ||
+ | if (goS) { | ||
+ | var southN =this.n+this.grid.w; | ||
+ | if (goW) result.push(southN-1);//SW | ||
+ | result.push(southN);//S | ||
+ | if (goE) result.push(southN+1);//SE | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | |||
+ | Cell.prototype.placeBomb = function() { | ||
+ | this.isBomb = true; | ||
+ | // var neighbors = this.neighbors(); | ||
+ | // for (var i=0; i<neighbors.length; i++) | ||
+ | // this.grid[neighbors[i]].count++; | ||
+ | } | ||
+ | |||
+ | Cell.prototype.reveal = function(skipCheck) { | ||
+ | if (this.isVisible || this.isFlag) return; | ||
+ | this.isVisible = true; | ||
+ | this.grid.numVisible++; | ||
+ | |||
+ | if (!skipCheck && this.isBomb) { | ||
+ | if (gui) gui.reveal(this.token,0,true); | ||
+ | this.grid.flagsAvail--; | ||
+ | this.grid.youLose(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | var neighbors = this.neighbors(); | ||
+ | var bombCount=0; | ||
+ | for (var n=0; n<neighbors.length; n++) { | ||
+ | if (this.grid[neighbors[n]].isBomb) | ||
+ | bombCount++; | ||
+ | } | ||
+ | |||
+ | if (gui) gui.reveal(this.token,bombCount,this.isBomb); | ||
+ | |||
+ | if (!skipCheck) this.grid.checkWin(); | ||
+ | |||
+ | if (bombCount==0) { | ||
+ | for (var i=0; i<neighbors.length; i++) | ||
+ | this.grid[neighbors[i]].reveal(true); | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | Cell.prototype.flag = function() { | ||
+ | if (this.isVisible) return; | ||
+ | //toggle on or off: | ||
+ | if (!this.isFlag) {//no flag | ||
+ | if (this.grid.flagsAvail==0) return;//none available | ||
+ | this.grid.flagsAvail--; | ||
+ | } else {//has flag, remove it | ||
+ | this.grid.flagsAvail++; | ||
+ | } | ||
+ | this.isFlag = !this.isFlag; | ||
+ | |||
+ | if (gui) gui.flag(this.token,this.isFlag); | ||
+ | this.grid.checkWin(); | ||
+ | } | ||
+ | |||
+ | //--- WraparoundCell --- | ||
+ | |||
+ | function WraparoundCell(n,grid) { | ||
+ | Cell.call(this,n,grid); | ||
+ | } | ||
+ | WraparoundCell.prototype=new Cell(0,null); | ||
+ | WraparoundCell.prototype.constructor = WraparoundCell; | ||
+ | |||
+ | WraparoundCell.prototype.neighbors = function() { | ||
+ | var w=this.grid.w; | ||
+ | var len=this.grid.length; | ||
+ | //delta added to n to obtain neighbor in each dir: | ||
+ | var goW = (this.x>0)?-1:w-1; | ||
+ | var goE = (this.x+1 < w)? 1:1-w; | ||
+ | var goN = (this.y>0)? -w:len-w; | ||
+ | var goS = (this.y+1 < this.grid.h)? w:w-len; | ||
+ | var result=[]; | ||
+ | |||
+ | var northN =this.n+goN; | ||
+ | var southN =this.n+goS; | ||
+ | result.push(northN+goW); | ||
+ | result.push(northN); | ||
+ | result.push(northN+goE); | ||
+ | result.push(this.n+goW); | ||
+ | result.push(this.n+goE); | ||
+ | result.push(southN+goW); | ||
+ | result.push(southN); | ||
+ | result.push(southN+goE); | ||
+ | |||
+ | return result; | ||
+ | } | ||
+ | |||
+ | // Start: | ||
+ | function makeMinefield(w,h,bombs) { | ||
+ | window.minefield= | ||
+ | new Grid(Cell,w,h,bombs); | ||
+ | } | ||
+ | function makeWrappingMinefield(w,h,bombs) { | ||
+ | window.minefield= | ||
+ | new Grid(WraparoundCell,w,h,bombs); | ||
+ | } | ||
+ | |||
+ | </pre></div></div> | ||
+ | |||
+ | === Interface === | ||
+ | The interface module has two parts: | ||
+ | * a file of javascript (minesweeper-gui.js) which manipulates the window's DOM | ||
+ | * a file of CSS rules (minesweeper-gui.css) which control the appearance of various DOM classes | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''minesweeper-gui.js''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | var gui = {numgrids:0}; | ||
+ | |||
+ | gui.doClick = function(ev) { | ||
+ | |||
+ | var isRightClick=false; | ||
+ | if ("which" in ev) | ||
+ | isRightClick = (ev.which >1); | ||
+ | if ("button" in ev) | ||
+ | isRightClick = (ev.button >1); | ||
+ | if (!isRightClick) | ||
+ | isRightClick = ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey; | ||
+ | |||
+ | if (isRightClick) | ||
+ | ev.target.cell.flag(); | ||
+ | else | ||
+ | ev.target.cell.reveal(); | ||
+ | } | ||
+ | |||
+ | gui.ensureRow = function(tableid,y) { | ||
+ | var rowid=tableid+'row'+y; | ||
+ | var tr = document.getElementById(rowid); | ||
+ | if (tr) return tr; //use existing row | ||
+ | |||
+ | // no such row; make it: | ||
+ | tr = document.createElement("tr"); | ||
+ | tr.id=rowid; | ||
+ | var table = document.getElementById(tableid); | ||
+ | table.appendChild(tr); | ||
+ | return tr; | ||
+ | } | ||
+ | |||
+ | gui.makeGrid = function(gridtoken,w,h) { | ||
+ | var newid = "minegrid"+this.numgrids; | ||
+ | this.numgrids++; | ||
+ | var table = document.createElement("table"); | ||
+ | table.id=newid; | ||
+ | var space = document.getElementById("gamespace"); | ||
+ | space.appendChild(table); | ||
+ | return newid; | ||
+ | } | ||
+ | |||
+ | gui.makeCell = function(tableid,celltoken,x,y) { | ||
+ | var tr = this.ensureRow(tableid,y); | ||
+ | var td = document.createElement("td"); | ||
+ | td.className = "unknown"; | ||
+ | td.onclick = this.doClick; | ||
+ | |||
+ | tr.appendChild(td); | ||
+ | td.cell = celltoken; | ||
+ | return td; | ||
+ | } | ||
+ | |||
+ | gui.reveal = function(td,count,isBomb) { | ||
+ | if (isBomb) { | ||
+ | td.innerHTML="*"; | ||
+ | td.className = "bomb"; | ||
+ | } else { | ||
+ | td.innerHTML = (count)? count: ""; | ||
+ | td.className = "clear count"+count; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | gui.flag = function(td,flagOn) { | ||
+ | td.innerHTML = flagOn? "#": ""; | ||
+ | } | ||
+ | </pre></div></div> | ||
+ | |||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''minesweeper-gui.css''' | ||
+ | <div class="mw-collapsible-content"> | ||
+ | <pre> | ||
+ | #gamespace table { | ||
+ | border:1px solid black; | ||
+ | margin:0px; | ||
+ | padding:0px; | ||
+ | text-align:center; | ||
+ | } | ||
+ | #gamespace td { | ||
+ | height:22px; | ||
+ | width:22px; | ||
+ | } | ||
+ | td.unknown { | ||
+ | background:#ddd; | ||
+ | } | ||
+ | td.bomb { | ||
+ | background:red; | ||
+ | } | ||
+ | td.clear { | ||
+ | background:white; | ||
+ | } | ||
+ | td.count1 { | ||
+ | color:blue; | ||
+ | } | ||
+ | td.count2 { | ||
+ | color:orange; | ||
+ | } | ||
+ | td.count3 { | ||
+ | color:red; | ||
+ | } | ||
+ | td.count4 { | ||
+ | color:magenta; | ||
+ | } | ||
+ | td.count5 { | ||
+ | color:brown; | ||
+ | } | ||
+ | </pre></div></div> | ||
+ | |||
+ | ===Running the application=== | ||
+ | To run this application on your own computer, you'll need to create files containing the code from the boxes above. | ||
+ | Using any text editor, in a single directory on your computer, create four empty files with the following names: | ||
+ | * mines7.js | ||
+ | * minesweeper-gui.js | ||
+ | * minesweeper-gui.css | ||
+ | * minesweeper.html | ||
+ | Then copy and paste the code from corresponding box above into each of the files, and save each file. | ||
+ | You can change the names if you want, but you'll need to change the corresponding string within "minesweeper.html" to match. | ||
+ | |||
+ | Finally, open the file "minesweeper.html" (or whatever you called it) within any web browser, and the application will run within that window. |
Latest revision as of 21:01, 10 February 2014
Announcements
Minesweeper demo code begins here.
General Information
This course is currently running November 2013-January 2014, Tuesdays 11:00-1:00.
Part 1 (November-December) will offer students a solid foundation in Javascript as a general-purpose programming language. Part 2 (January) will explore Javascript's integration in web browsers with HTML, CSS, and the Document Object Model (DOM).
Instructor: Dan Bauer (email: dsbauer at gmail)
Course Outline
Week 1: Values and Expressions
- Firebug Console/Interpreter
- The console allows a dialogue with Javascript interpreter: you make one (possibly compound) statement, it replies with one value.
- Expressions
- An expression (EXPR) can be:
- a primitive (e.g. 1)
- a variable (e.g. x)
- an operator combining smaller EXPR's (e.g. x+1, 2*(x+1))
- An expression (EXPR) can be:
- Operators have:
- a keyword or symbol (OP)
- a syntax template (e.g. _ OP _)
- unary prefix: OP_
- unary postfix: _OP
- binary infix: _OP_
- ternary infix: _OP_OP_
- one or more inputs _ ("operands")
- one output ("return value")
- may be undefined
- possible side-effects (e.g. change in variable's value)
- Variables have:
- a name/identifier
- a value (initially undefined)
- each value has a type (primitive or reference)
- a scope
- Other (non-expression) Statements
- var NAME;
- var NAME = EXPR;
- if (EXPR) {EXPR;}
- if (EXPR) {EXPR;} else {EXPR;}
- Primitive Types
- undefined
- Number
- same type for integers and floats (fractional)
- special values: NaN ("not a number"), Infinity
- String
- wrapped in either " " or ' '
- may contain opposite quote or other special characters
- \t (tab)
- \n (newline)
- may be empty ("")
- Boolean (true/false)
- false-ish values: false,undefined,null,0,"",NaN
- (some) true-ish values: true,1,"false","undefined","0",{} (empty object)
Some Sample Operators:
operator | input type | example | result | result type | side effects |
---|---|---|---|---|---|
typeof | NUM | typeof 1 | "number" | STR | |
STR | typeof "word" | "string" | |||
BOOL | typeof true | "boolean" | |||
,
(sequence) |
any | "a", "b", "c" | "c" | type of
last value | |
(4,"3"),("two",1) | 1 | ||||
+
(plus) |
NUM | 1+1 | 2 | NUM | |
+
(concatenate) |
STR | "1"+"1" | "11" | STR | |
mixed | 1+"2" | "12" | STR | ||
- * / % | NUM | 3-1 | 2 | NUM | |
STR | "3"-"1" | 2 | |||
mixed | "3"-1 | 2 | |||
< > <= >=
(comparison) |
NUM | 1<2 | true | BOOL | |
STR | "b"<"a" | false | |||
==
(equality) |
NUM | 1==2 | false | BOOL | |
STR | "a"=="aa" | false | |||
mixed | 1=="1" | true | |||
mixed | 0==false | true | |||
===
(identity) |
NUM | 1===2 | false | BOOL | |
STR | "abc"==="abc" | true | |||
mixed | 1==="1" | false | |||
mixed | 1===true | false | |||
!
(not) |
any | !1 | false | BOOL | |
!"" | true | ||||
&&
(and) |
any | null && "red" | null | first false-ish
or last input | |
1 && "green" | "green" | ||||
||
(or) |
any | "yes" || null | "yes" | first true-ish
or last input | |
"" || 0 | 0 | ||||
?:
(conditional) |
any | true? "yes": "no" | "yes" | any | |
variable assignment operators | x is now... | ||||
=
(assignment) |
any | x=1 | 1 | same as input | 1 |
x = "blue" | "blue" | "blue" | |||
+=
(incremental assignment) |
NUM | x=0; x+=2; | 2 | NUM | 2 |
STR | x="he"; x+="llo"; | "hello" | STR | "hello" | |
mixed | x=""; x+=2; | "2" | STR | "2" | |
++
(post-increment) |
NUM | x=0; x++; | 0 | NUM | 1 |
STR | x=""; x++; | 0 | 1 | ||
++
(pre-increment) |
NUM | x=0; ++x; | 1 | NUM | 1 |
STR | x=""; ++x; | 1 | 1 |
Week 2: Loops and Functions
Nov.12 in-class scratchpad
/* * This is a JavaScript Scratchpad. * * Enter some JavaScript, then Right Click or choose from the Execute Menu: * 1. Run to evaluate the selected text (Ctrl+R), * 2. Inspect to bring up an Object Inspector on the result (Ctrl+I), or, * 3. Display to insert the result in a comment after the selection. (Ctrl+L) */ //function countdown() { var countdown = function (start) { var list=""; // generate string of numbers from 10 to 0 for (var i=start; i>0; i--) { list += (i + " "); } return list; } countdown var alias=countdown; alias(7); alias(8); /* 8 7 6 5 4 3 2 1 *//* 7 6 5 4 3 2 1 */ alias = 7; /* 10 9 8 7 6 5 4 3 2 1 */ /* function countdown() { var list=""; // generate string of numbers from 10 to 0 for (var i=10; i>0; i--) { list += (i + " "); } } */ countdown(); /* 10 9 8 7 6 5 4 3 2 1 */ /* 10 9 8 7 6 5 4 3 2 1 */ /* undefined */ function countdown() {} function plus(x,y) { return x+y; } plus(1,2); /* 3 */
Loops
Loops have:
- some form of counter ("iterator"), a variable which changes with each cycle of the loop
- an initialization (INIT)
- a counter increment/decrement (CHANGE)
- a repeated ACTION
- a continuation condition (COND)
- any COND expression will be treated as a Boolean, either true-ish or false-ish
- the ACTIONs and CHANGE occur only when COND is true-ish
Loop Statements:
Loop keyword | Pattern | Example |
---|---|---|
while |
INIT; while(COND) { ACTION; ... CHANGE; } |
var i=0; while (i<10) { alert(i); i++; } |
for |
for (INIT; COND; CHANGE) { ACTION; ... } |
for (var i=0; i<10; i++) { alert(i); } |
- Sometimes the COND includes the CHANGE, e.g.:
for (var i=0; (i++)<10; ) {...} for (var i=10; --i; ) {...}
- ACTIONs can be any statements, including nested loops
Functions
Functions:
- are mini-programs, independent modules which can be reused in multiple contexts
- are a kind of Object, which can be referenced by multiple names
- have parameters which can change for each call/use
- return one value of any type (or undefined if unspecified)
- the keyword return specifies the returned value and exits the function immediately
- create a separate space ("call object","execution context", or "scope") for their local variables and parameters
- Each call creates a separate scope.
- Scopes last as long as needed, then destroyed automatically.
Function-specific Operators:
operator | input type | example | result | result type | side effects |
---|---|---|---|---|---|
function NAME(PARAMS){BODY} function(PARAMS){BODY} (define function) |
function add(x,y) {return x+y;} | a function | Function | creates Function object,
creates (or redefines) var. NAME if included | |
NAME(ARGS) (call function) |
NAME: Function,
ARGS: any |
add(1,2) | 3 | any | depends on function body |
Exercises (Set 1)
1) Write a function to calculate, for a group of N people where everyone shakes the hand of everyone else, how many total handshakes there are.
Solution...
function numPairs(setSize) { return setSize*(setSize-1)/2; }
Write another function to enumerate these interactions, returning a single string describing them all. People may be identified by number. For example, for N=3, it might return: "#1 meets #2. #1 meets #3. #2 meets #3."
(Hint: you may want a second "helper" function to handle a single interaction.)
Solution...
function writePair(personA,personB) { return "#"+personA+" meets #"+personB+". "; } function enumeratePairs(numPeople) { var result=""; for (var personA=1; personA<=numPeople; personA++) { for (var personB=personA+1; personB<=numPeople; personB++) { result+=writePair(personA,personB); } } return result; }
2) Write a function to decide whether a given integer is prime. You may need some of these functions/operators:
- function Math.floor(N): truncates any fractional part of a number N (i.e. returns greatest int <=N)
- function Number.isInteger(N): returns true is N is an integer
- modulo operator %: x%y returns 0 if x divides evenly by y
Solution...
function isPrime(n) { if (n==0 || n==1 || !Number.isInteger(n)) return false; // excludes 0,1,and non-integers // search for number which divides n evenly... var sqrt=Math.sqrt(n); // only need to search up to n's square root for (var test=2; test<=sqrt; test++) { if (Number.isInteger(n/test)) return false; //found one; n isn't prime } return true;//n must be prime }
3) Imagine that a deck of playing cards is sorted by rank and suit: first all the Aces, then the Twos, etc, with the Kings last. Within each rank, the suits are in the order Heart, Diamond, Spade, Club. Number each card in order from 0 to 51 (i.e. 0=Ace of Hearts; 51=King of Clubs), and let that ID number represent the corresponding card.
Following that encoding scheme, write a set of functions to compute different features/relations of the cards:
- rank(card) returns 1-13, representing the card's rank.
- suit(card) returns 1-4, representing the card's suit.
- cardNum(rank,suit) returns 0-51, identifying the card of a given rank and suit.
- color(card) returns "red" or "black".
- precedes(cardA,cardB) returns true only if cardA is one less in rank than cardB. Assume rank wrap-around (King precedes Ace precedes Two).
- sameColor(cardA,cardB) returns true only if cardA and cardB have the same color.
- nextInSuit(cardA) returns cardB, the ID number of the card following cardA in the same suit. Assume wrap-around.
- prevInSuit(cardB) returns cardA, the ID number of the card preceding cardB in the same suit.
Your functions may call each other. Try to reuse their functionality to avoid duplicating code.
Initially, assume each function is given valid arguments (i.e. the args are integers in the appropriate range). Later, devise a system for dealing with arguments which are invalid in various ways, and rewrite your functions to tolerate such errors whenever possible.
Solution: simple version...
function rank(card) { // --> 1..13 return Math.floor(card/4)+1; } function suit(card) { // --> 1..4 return (card%4)+1; } function cardNum(rank,suit) { // --> 0..51 return (rank-1)*4 + (suit-1); } function color(card) { // -->"red,"black" var theSuit=suit(card); return (theSuit<3)? "red": "black"; } function precedes(cardA,cardB) { //-->false,true var diff= rank(cardB)-rank(cardA); return diff==1 || diff==-12; } function sameColor(cardA,cardB) { //-->false,true return color(cardA)==color(cardB); } function nextInSuit(cardA) {//--> 0..51 nextCard = cardA+4; if (nextCard>51) nextCard-=52; return nextCard; } function prevInSuit(cardB) {//--> 0..51 prevCard = cardB-4; if (prevCard<0) prevCard+=52; return prevCard; }
Solution: error-detecting version...
// Helper function; ensures num is integer between low, high // Returns true if valid, otherwise NaN function isValid(num,low,high) { // Possible return values--> NaN, true if ((typeof num)!="number") //wrong type return NaN; if (!Number.isInteger(num)) //non-integer return NaN; if (num<low || num>high) //out of range return NaN; return true; } function rank(card) { // --> 1..13, NaN return isValid(card,0,51) && Math.floor(card/4)+1; } function suit(card) { // --> 1..4, NaN return isValid(card,0,51) && (card%4)+1; } function cardNum(rank,suit) { // --> 0..51, NaN return isValid(rank,1,13) && isValid(suit,1,4) && ((rank-1)*4 + (suit-1)); } function color(card) { // -->"red,"black",NaN var theSuit=suit(card); if (isNaN(theSuit)) return NaN; return (theSuit<3)? "red": "black"; } function precedes(cardA,cardB) { //-->false,true,NaN var diff= rank(cardB)-rank(cardA); if (isNaN(diff)) return NaN; return diff==1 || diff==-12; } function sameColor(cardA,cardB) { //-->false,true,NaN var colorA=color(cardA), colorB=color(cardB); if (Number.isNaN(colorA) || Number.isNaN(colorB)) // must use Number.isNaN() instead of isNaN(), which returns true for non-numeric strings return NaN; return colorA==colorB; } function nextInSuit(cardA) {//--> 0..51,NaN nextCard = isValid(cardA,0,51) && cardA+4; if (nextCard>51) nextCard-=52; return nextCard; } function prevInSuit(cardB) {//--> 0..51,NaN prevCard = isValid(cardB,0,51) && cardB-4; if (prevCard<0) prevCard+=52; return prevCard; }
Week 3: Objects, Arrays, and Methods
Objects
An object is a collection of named "properties" or "members", similar to variables...
Variables... | Properties... |
---|---|
live in a global or function "scope" (execution context) | live in an object |
need no prefix | are (usually) prefixed with their object and an operator (. or []) |
must be declared with var | are created merely by assigment |
cause an error if not declared | return undefined if not declared |
have naming restrictions | may be named with any string |
last until their scope expires | may be deleted |
Object creation syntax:
- using new operator:
var empty=new Object(); // empty object
var rect=new Object(); //empty object rect.shape= "rectangle"; //set a property rect.numsides=4; //set another property rect.blank= new Object(); //set another property, an nested empty object
- using object-literal notation (JSON):
var obj = {}; //empty object
var rect = {shape:"rectangle", numsides:4, blank:{}}; // object with 3 initial properties
Property syntax:
- using the dot operator:
rect.numsides; //4 rect.blank.innerProp; //undefined
- using the [] (member) operator:
rect["numsides"];//4 rect[numsides]; //Reference Error (unless numsides is a variable)
operator | input type | example | result | result type | side effect |
---|---|---|---|---|---|
var obj={name:"Bubba"} | |||||
_._
(dot) |
OBJ,PROP | obj.name | "Bubba" | any | |
_[_]
(index/member/"sub") |
OBJ,STR | obj["name"] | "Bubba" | any | |
delete _._ | OBJ,PROP | delete obj.name | true | BOOL
(true if found, else false) |
removes property |
new _ | constructor | var obj = new Object(); | an object | OBJ | allocates object space |
_ in _ | STR,OBJ | ("name" in obj) | true | BOOL |
Methods:
- are functions which are properties of an object.
- use the keyword this to refer to their object (not themselves)
- Examples:
- Math.sqrt()
- Math.floor()
- Number.isInteger()
Arrays
An array is an object containing a series of integer-indexed values (i.e. properties whose names are integers).
- Indexes always begin with 0 (i.e. the first element has index 0)
- Therefore, the last element always has an index of (length-1)
A "pseudo-array" is any object with some properties named by integers:
var arr={0:'first', 1:'second', 3:'fourth', length:4};//third is implicit but undefined
- a length property is optional and must be set manually
A genuine Array is a special class of object which...
- automatically coordinates its integer-named properties with a special length property.
- When creating new properties with index >= length, length extends to include it:
var arr=['a','b','c']; arr.length; //3 arr[9]='j'; arr.length; //10
- When setting length to a greater value, new properties (with value undefined) fill in the end
- When setting length to a lesser value, properties beyond that new range are deleted
- When creating new properties with index >= length, length extends to include it:
- has a predefined set of useful methods:
- push(VAL): modifies array, adding VAL to the end
- pop(): modifies array, removing last value and returning it
- join(DELIM): creates string by concatenating array elements with DELIM between them. The converse of join is String.split(DELIM), which divides a string and creates an array.
- many more...
Array-creation syntax
var arr=new Array(); //empty array var arr = []; //empty array var arr = [1,2,3,"four",{},[]]; //array with six elements, including an empty Object and empty Array
Wrapper Objects
Primitives may be manually or automatically wrapped in type-specific objects which allow them properties and built-in methods:
- Numbers:
var four= Object(4); //number wrapper object typeof four;//"object" four== 4;//true four===4;//false four.valueOf();//4 four*3; //12
- Strings:
var xyz= Object("x,y,z"); //string wrapper object xyz== "x,y,z";//true xyz==="x,y,z";//false xyz.length;//5 xyz[2];//"y" xyz.split(",");//array ["x","y","z"]
String primitives auto-convert to wrapper objects:
"a b c".split(" ");//array ["a","b","c"] "a b c"[4];//"b"
Exercises (Set 2)
4) Revisit Exercise #1b. Build a data structure which represents all people and their meetings...
a) Assume there are 5 people. Give them each a name. Create an array called people of length 5 holding their names. For these exercises, you may use a global variable for people, but recognize that's a dangerous practice in real programming!
Solution...
var people=['Alan','Betty','Carl','Dana','Emma'];
b) Rewrite your pair-enumeration function to use your people array. Use the array size to simulate all meetings and substitute people's names for numbers, as in: "Alan meets Betty. Alan meets Carl. Betty meets Carl..."
Solution...
function writePair(personA,personB) { return people[personA]+" meets "+people[personB]+". "; }
function enumeratePairs() { var result=""; for (var personA=0; personA<(people.length-1); personA++) { for (var personB=personA+1; personB<people.length; personB++) { result+=writePair(personA,personB); } } return result; }
c) Instead of constructing your people array manually, write a function to do it incrementally. Function addPerson(name) should add name to the array each time it's called.
Solution...
var people=[]; function addPerson(name) { people.push(name); }
d) Now represent each person with an object. Each object should have a name property. Modify your addPerson function to fill the people array with objects instead of name strings. Change your enumeration function as needed to work with the object array.
Solution...
function addPerson(name) { people.push({name:name}); } function writePair(indexA,indexB) { // retrieve objects by index: var personA=people[indexA]; var personB=people[indexB]; return personA.name+" meets "+personB.name+". "; }
e) Make your data structure remember all the meetings. To each object representing a person P in the people array, add a property friends which stores an array of the names of all people whom P has met. Change your enumerate function to add names to these lists with each meeting.
Solution...
function addPerson(name) { people.push({name:name, friends:[]}); } function writePair(indexA,indexB) { //retrieve objects: var personA=people[indexA]; var personB=people[indexB]; personA.friends.push(personB.name); personB.friends.push(personA.name); return personA.name+" meets "+personB.name+". "; }
f) Change the friends array of each person to store not the names but the objects of other people. Remember that an object can have multiple references. You need to have only one object per person, but with references to it in both people and multiple friends arrays.
Solution...
function writePair(indexA,indexB) { //retrieve objects: var personA=people[indexA]; var personB=people[indexB]; personA.friends.push(personB); personB.friends.push(personA); return personA.name+" meets "+personB.name+". "; }
g) Once all meetings have been recorded and all friends lists built, write a function newestFriendOf(personNum) which returns the name of the last person met by person # personNum.
Solution...
function newestFriendOf(personNum) { var person = people[personNum]; var friend = person.friends[person.friends.length-1]; return friend.name;
}
h) Turn all of your functions into methods of the people array. When a function needs to refer to the people array, use the keyword this instead.
Solution...
var people=[];
people.addPerson = function(name) { this.push({name:name, friends:[]}); }
people.writePair = function(indexA,indexB) { //retrieve objects: var personA=this[indexA]; var personB=this[indexB]; personA.friends.push(personB); personB.friends.push(personA); return personA.name+" meets "+personB.name+". "; }
people.enumeratePairs = function() { var result=""; for (var personA=0; personA<(this.length-1); personA++) { for (var personB=personA+1; personB<this.length; personB++) { result+=this.writePair(personA,personB); } } return result; }
people.newestFriendOf = function(personNum) { var person = this[personNum]; var friend = person.friends[person.friends.length-1]; return friend.name; }
Week 4: Practice with Objects, Arrays, and Functions
This week is a break from new content, instead offering more practice with real code using Objects, Arrays, and Functions.
Demo Code
These code samples begin with a portion of Exercise #3 (modelling playing cards) and improve the design incrementally in successive versions. You should understand the differences between versions and the rationale for each change.
Version 0: sample from exercise #3
//===== Version 0: Global functions with card argument (from Exercise #3)
function rank(cardnum) { return Math.floor(cardnum/4)+1;// --> 1..13 } function suit(cardnum) { return cardnum%4+1; // --> 1..4 } function color(cardnum) { return (suit(cardnum)<3)? "red":"black"; } //etc...
//Testing: rank(51); // 13 (King) suit(51); // 4 (Clubs) color(51); // "black"
Version 1: adding card names
//===== Version 1: Add card names
function rank(cardnum) { return Math.floor(cardnum/4)+1;// --> 1..13 } function suit(cardnum) { return cardnum%4+1; // --> 1..4 } function color(cardnum) { return (suit(cardnum)<3)? "red":"black"; } //etc...
function name(cardnum) { return rankNames[rank(cardnum)] + " of " + suitNames[suit(cardnum)]; }
var rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; var suitNames = ["","Hearts","Diamonds","Spades","Clubs"];
//Testing: rank(51); //13 suit(51); //4 color(51); // "black" name(51); // King of Clubs
Version 2a: single controller object
//======== Version 2: Methods of single object var cardReader = {}; // "toolkit" object containing all card functions
cardReader.rank = function(cardnum) { return Math.floor(cardnum/4)+1;// --> 1..13 } cardReader.suit = function(cardnum) { return cardnum%4+1; // --> 1..4 } cardReader.color = function(cardnum) { return (this.suit(cardnum)<3)? "red":"black"; }
cardReader.name = function(cardnum) { return this.rankNames[this.rank(cardnum)] +" of "+ this.suitNames[this.suit(cardnum)]; }
cardReader.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; cardReader.suitNames = ["","Hearts","Diamonds","Spades","Clubs"];
//Testing: cardReader.rank(51); //13 cardReader.suit(51); //4 cardReader.color(51); // "black" cardReader.name(51); // King of Clubs
Version 2b: single controller object (alternate form)
//======== Version 2b: Methods of single object (object literal/JSON form) var cardReader = { // "toolkit" object containing all card functions
rank: function(cardnum) { return Math.floor(cardnum/4)+1;// --> 1..13 }, suit: function(cardnum) { return cardnum%4+1; // --> 1..4 }, color: function(cardnum) { return (this.suit(cardnum)<3)? "red":"black"; },
name: function(cardnum) { return this.rankNames[this.rank(cardnum)] +" of "+ this.suitNames[this.suit(cardnum)]; },
rankNames: ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"], suitNames: ["","Hearts","Diamonds","Spades","Clubs"] }; //end cardReader
//Testing: cardReader.rank(51); //13 cardReader.suit(51); //4 cardReader.color(51); // "black" cardReader.name(51); // King of Clubs
Version 3: single maker object, multiple instance objects
//===== Version 3: Card as object (w. own methods & cached data fields) // Each card is now an object, created by function makeCard function makeCard(cardnum) { var card = {};
//computed initially: card.rank = Math.floor(cardnum/4)+1; card.suit = cardnum%4+1;
//deferred: card.color = function() { return (this.suit<3)? "red":"black"; }
card.name = function() { return (rankNames[this.rank] +" of "+ suitNames[this.suit]); }
var rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; var suitNames = ["","Hearts","Diamonds","Spades","Clubs"];
return card; }
//Testing: var card51 = makeCard(51); card51.rank; //13 card51.suit; //4 card51.color(); //"black" card51.name(); //King of Clubs
Version 4: multiple instance objects sharing arrays
//===== Version 4: Card as object (as before), but arrays are shared
function makeCard(cardnum) { var card = {};
//computed initially: card.rank = Math.floor(cardnum/4)+1; card.suit = cardnum%4+1;
//deferred: card.color = function() { return (this.suit<3)? "red":"black"; }
card.name = function() { return (makeCard.rankNames[this.rank] +" of "+ makeCard.suitNames[this.suit]); }
return card; }
//Arrays created AFTER function, attached to it: makeCard.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; makeCard.suitNames = ["","Hearts","Diamonds","Spades","Clubs"];
//Testing: var card51 = makeCard(51); card51.rank; //13 card51.suit; //4 card51.color(); //"black" card51.name(); //King of Clubs
Week 5: Constructors, Prototypes, and Inheritance
Constructors
A constructor is simply a function whose purpose is to initialize an object (usually empty) to behave as an instance of the "class" the constructor represents. There are no "class" objects in JS, but constructors are the closest approximation.
Think of a constructor as a temporary method which is attached to an object only for one execution, during which it creates and sets the appropriate object properties.
A constructor has:
- no creation of its intended object (done beforehand by the new operator);
- no return value;
- the keyword this, referring to its temporary "owner" object;
- by convention, a name which is a noun phrase starting with capital letter (e.g. Card, BananaCremePie, PresidentsOfMars)
Some predefined constructors:
- Object
- Array
- Function
- Number
- String
The new operator
Constructors are normally called with a special operator, new, which creates an empty object and applies the constructor to it. The expression new CTOR(args) does (approximately) the following steps:
- create an empty object OBJ
- link OBJ to CTOR's prototype (see #Prototypes)
- temporarily reinterpret the word this to refer to OBJ
- executes CTOR(args), which initializes OBJ using this
- restore the previous meaning of this
- return OBJ as the expression's value
Using predefined constructors:
var obj; obj = new Object(); //--> return empty Object, i.e. {} obj = new Array(); //-->empty Array, i.e. [] obj = new Array(1,2,3); //--> initialized Array [1,2,3] obj = new Array(3); //--> Array of 3 undefineds: [undefined,undefined,undefined], NOT [3] obj = new Function('x','y','return x+y;');//--> function(x,y) {return x+y;} obj = new String("hello"); //wrapper obj containing "hello", NOT primitive string "hello" obj = new Number(12); //wrapper obj containing 12, NOT primitive number 12
Constructors without new
If new is omitted, some predefined constructors return primitives:
obj = String("hello");// primitive string "hello" obj = Number(12);// primitive number 12
Others return objects:
obj = Object(5);//wrapper obj containing 5 obj = Array(1,2,3); //Array object [1,2,3]
But such behavior is atypical; in general, you should always use new with constructors!
Except for special cases, calling a constructor without using new means that its reference this points to the wrong object! Usually it points to the global object, so that constructors will wrongly create global properties:
function BlueSquare() {this.color="blue"; this.sides=4;} //a constructor obj = new BlueSquare();// CORRECT, returns object: {color:"blue", sides:4} obj = BlueSquare(); // WRONG... obj; //--> undefined, function has no return val color; //--> "blue" sides; //--> 4
Constructors as Classes
Constructors unify their instances (the objects they initialize) into a family or "class":
- All instances have a built-in constructor property which remembers the constructor which made them. It can be a convenient way to check the subtype of an object:
obj = new Card(); obj.constructor == Card; //true
- Alternatively, the operator instanceof returns true for certain combinations of instance and constructor:
obj instanceof Card; //true obj instanceof Object; //true (everything is an Object) Card instanceof Object; //true Function instanceof Object; //true Card instanceof Function; //true (every Ctor is a Function) Object instanceof Function; //true Function instanceof Function; //true
- Properties added to a constructor can store resources (e.g. Arrays) shared by all its instances (as can #Prototypes);
- Methods added to a constructor can compute class information without needing any particular instance. For example:
function Tower(hgt) { //constructor makes towers, remembers highest this.height=hgt; if (hgt > Tower.highestEver()) { Tower.highestEver = function() {return hgt;} //revise definition } } Tower.highestEver = function() {return 0;} //initial definition
Demo code, part 2
Continuing improvements to cards demo...
Version 5: Card constructor
//===== Version 5: Card constructor
// makeCard (optional) is a wrapper which calls Card constructor function makeCard(cardnum) { return new Card(cardnum); }
// Constructor: function Card(cardnum) { this.rank = Math.floor(cardnum/4)+1; this.suit = cardnum%4+1;
this.color = function() { return (this.suit<3)? "red":"black"; } this.name = function() { return (Card.rankNames[this.rank] +" of " +Card.suitNames[this.suit]); } //no return needed }
Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"];
//Testing: var card51 = new Card(51); card51.color(); //black card51.name(); //King of Clubs
Version 6: Card constructor w. shared prototype methods
//===== Version 6: Card constructor + prototype function makeCard(cardnum) { return new Card(cardnum); } // Constructor: function Card(cardnum) { this.rank = Math.floor(cardnum/4)+1; this.suit = cardnum%4+1; } // Shared methods: Card.prototype.name = function() { return (Card.rankNames[this.rank] +" of " +Card.suitNames[this.suit]); } Card.prototype.color = function() { return (this.suit<3)? "red":"black"; } Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"]; //Testing: var card51 = new Card(51); card51.name(); // --> King of Clubs
Version 7: Card subclassing, inheritance
//==== Version 7: Card subclassing // Superclass constructor: function Card(cardnum) { this.rank = Math.floor(cardnum/4)+1; this.suit = cardnum%4+1; } Card.prototype.name = function() { return (this.constructor.rankNames[this.rank] +" of " +this.constructor.suitNames[this.suit]); } Card.prototype.color = function() { return (this.suit<3)? "red":"black"; } Card.rankNames = ["","Ace","Two","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"]; Card.suitNames = ["","Hearts","Diamonds","Spades","Clubs"]; // Subclass constructor: (Minor Arcana of Tarot deck) function MinorArcana(cardnum,orientation) { Card.call(this,cardnum); //initialize as superclass (Card) //add subclass-specific properties: this.orientation = orientation; } MinorArcana.rankNames = Card.rankNames.slice(0); //copy all MinorArcana.rankNames.splice(11,1,"Page","Knight");//replace J w. P+K MinorArcana.suitNames = ["","Cups","Coins","Swords","Wands"]; MinorArcana.prototype = new Card(); MinorArcana.prototype.constructor = MinorArcana; //Testing: var normal42 = new Card(42); normal42.name(); // --> Jack of Spades var tarot42 = new MinorArcana(42,"upright"); tarot42.name(); // --> Page of Swords tarot42.orientation; //--> upright
The call method
Some explanation is needed for this mysterious line in Version 7:
Card.call(this,cardnum)
Every function has a predefined method (i.e. a member function) named call which executes the function, similarly to the call operator _(_). The difference is that a function's call method expects one additional argument, an object, in the first position, and it calls its owner function as if that function were a method of that object. For example:
var dot = {radius:1, color:"white"}; function paint(newcolor) { this.color = newcolor; } paint.call(dot,"blue");//calls paint as if it belongs to dot (i.e. this==dot) dot.color;//--> "blue"
This "temporary ownership" can be used to apply a constructor manually (without new) to initialize an existing object.
In the demo line
Card.call(this,cardnum)
a subclass of Card is using Card as an initializer in the first step of its own initialization.
Advanced Topic: Building a simple application (Minesweeper)
Overview
We're building a simple version of the game Minesweeper. Like many applications, this one will separate the roles of Model and View into two modules:
- the game engine (Model) will manage the abstract structure and rules of the game;
- the interface (View) will control the display and user input. If time permits, we'll write more than one interface module to illustrate that the same Model works with both.
Outer HTML file
The modules are combined into a single application by including the javascript (and any CSS) for each in a single HTML file.
minesweeper.html
<html> <head> <meta charset="utf-8"> <script type="text/javascript" src="mines7.js"></script> <script type="text/javascript" src="minesweeper-gui.js"></script> <link type="text/css" rel="StyleSheet" href="minesweeper-gui.css"> </head> <body onload="makeMinefield(10,10,15);"> <div id="gamespace"></div> </body> </html>
This file is just a preview; it won't work until we've finished the components.
Game engine/model
Version 1: Basic grid geometry
mines1.js
// Stage 1: Grid and Cell objects + basic coordinate system //--- Grid --- function Grid(w,h,bombs) { this.length = w*h; this.w = w; this.h = h; for (var n=0; n<this.length; n++) { this[n] = new Cell(n,this); } } Grid.prototype.XYtoN = function(x,y) { return (y*this.w)+x;//-->n } Grid.prototype.NtoX = function(n) { return n % this.w;//-->x } Grid.prototype.NtoY = function(n) { return Math.floor(n/this.w);//-->y } //--- Cell --- function Cell(n,grid) { this.grid = grid; this.n = n; this.x = grid.NtoX(n); this.y = grid.NtoY(n); } Cell.prototype.neighbors = function() { //returns list of grid positions // four booleans (true if NOT adjacent to respective grid edge): var goW = (this.x>0); var goE = (this.x+1 < this.grid.w); var goN = (this.y>0); var goS = (this.y+1 < this.grid.h); var result=[]; if (goN) { var northN = this.n-this.grid.w; if (goW) result.push(northN-1);//NW result.push(northN);//N if (goE) result.push(northN+1);//NE } if (goW) result.push(this.n-1);//W if (goE) result.push(this.n+1);//E if (goS) { var southN =this.n+this.grid.w; if (goW) result.push(southN-1);//SW result.push(southN);//S if (goE) result.push(southN+1);//SE } return result; } // Start: function makeMinefield(w,h,bombs) { window.minefield= new Grid(w,h,bombs); }
Related Exercise
Implement a similar coordinate system in 3D--- specifically, for a Rubik's Cube. Imagine that each of the 27 smaller cubes (include the center) in a Rubik's Cube is numbered in two different ways:
- each has a unique number N, from 0 to 26, and
- each has a unique three-component coordinate (X,Y,Z), each component from 0 to 2, describing its position in the big cube.
Write four functions to convert between these two systems:
- function XYZtoN(x,y,z) should convert (X,Y,Z) to N
- functions NtoX(n), NtoY(n), and NtoZ(n) should each convert N to one component of (X,Y,Z)
There are many solutions!
Version 6: Full Game
mines6.js
// Stage 1: basic coordinate system // Stage 2: add mines,count // Stage 3: add visibility,reveal // Stage 4: add flags // Stage 5: add win/lose // Stage 6: attach UI! if (typeof gui == "undefined") var gui = null; //--- Grid --- function Grid(w,h,bombs) { this.length = w*h; this.w = w; this.h = h; this.numVisible = 0; if (gui) this.token = gui.makeGrid(this,w,h); for (var n=0; n<this.length; n++) { this[n] = new Cell(n,this); } //limit bomb density to <1/4 var maxbombs = Math.floor(this.length/4); if (bombs>maxbombs) bombs=maxbombs; this.numBombs = bombs; this.flagsAvail = bombs; while (bombs) { n = Math.floor(Math.random()*this.length); var cell = this[n]; if (!cell.isBomb) { cell.placeBomb(); bombs--; } } } Grid.prototype.XYtoN = function(x,y) { return (y*this.w)+x;//-->n } Grid.prototype.NtoX = function(n) { return n % this.w;//-->x } Grid.prototype.NtoY = function(n) { return Math.floor(n/this.w);//-->y } Grid.prototype.reveal = function(x,y) { var n=this.XYtoN(x,y); this[n].reveal(); } Grid.prototype.revealAll = function() { for (var n=0; n<this.length; n++) { this[n].reveal(true); } } Grid.prototype.flag = function(x,y) { var n=this.XYtoN(x,y); this[n].flag(); } Grid.prototype.checkWin = function() { if (this.flagsAvail==0 && (this.numVisible>= this.length-this.numBombs)) this.youWin(); } Grid.prototype.youWin = function() { alert("You win!"); } Grid.prototype.youLose = function() { alert("KABOOM!"); this.revealAll(); } //--- Cell --- function Cell(n,grid) { this.grid = grid; this.n = n; this.x = grid.NtoX(n); this.y = grid.NtoY(n); this.count = 0; this.isBomb = false; this.isVisible = false; this.isFlag = false; if (gui) this.token=gui.makeCell(grid.token,this,this.x,this.y); } Cell.prototype.neighbors = function() { //returns list of grid positions // four booleans (true if NOT adjacent to respective grid edge): var goW = (this.x>0); var goE = (this.x+1 < this.grid.w); var goN = (this.y>0); var goS = (this.y+1 < this.grid.h); var result=[]; if (goN) { var northN = this.n-this.grid.w; if (goW) result.push(northN-1);//NW result.push(northN);//N if (goE) result.push(northN+1);//NE } if (goW) result.push(this.n-1);//W if (goE) result.push(this.n+1);//E if (goS) { var southN =this.n+this.grid.w; if (goW) result.push(southN-1);//SW result.push(southN);//S if (goE) result.push(southN+1);//SE } return result; } Cell.prototype.placeBomb = function() { this.isBomb = true; var neighbors = this.neighbors(); for (var i=0; i<neighbors.length; i++) this.grid[neighbors[i]].count++; } Cell.prototype.reveal = function(skipCheck) { if (this.isVisible || this.isFlag) return; this.isVisible = true; this.grid.numVisible++; if (gui) gui.reveal(this.token,this.count,this.isBomb); if (!skipCheck) { if (this.isBomb) { this.grid.flagsAvail--; this.grid.youLose(); return; } this.grid.checkWin(); } if (this.count==0) { var neighbors = this.neighbors(); for (var i=0; i<neighbors.length; i++) this.grid[neighbors[i]].reveal(); } } Cell.prototype.flag = function() { if (this.isVisible) return; //toggle on or off: if (!this.isFlag) {//no flag if (this.grid.flagsAvail==0) return;//none available this.grid.flagsAvail--; } else {//has flag, remove it this.grid.flagsAvail++; } this.isFlag = !this.isFlag; if (gui) gui.flag(this.token,this.isFlag); this.grid.checkWin(); } // Start: function makeMinefield(w,h,bombs) { window.minefield= new Grid(w,h,bombs); }
Version 7: Bonus Variant: Wrap-around grid
mines7.js
// Stage 1: basic coordinate system // Stage 2: add mines,count // Stage 3: add visibility,reveal // Stage 4: add flags // Stage 5: add win/lose // Stage 6: attach UI! // Stage 7: subclass Cell for wrap-around if (typeof gui == "undefined") var gui = null; //--- Grid --- function Grid(celltype,w,h,bombs) { this.length = w*h; this.w = w; this.h = h; this.numVisible = 0; if (gui) this.token = gui.makeGrid(this,w,h); for (var n=0; n<this.length; n++) { this[n] = new celltype(n,this); } //limit bomb density to <1/4 var maxbombs = Math.floor(this.length/4); if (bombs>maxbombs) bombs=maxbombs; this.numBombs = bombs; this.flagsAvail = bombs; while (bombs) { n = Math.floor(Math.random()*this.length); var cell = this[n]; if (!cell.isBomb) { cell.placeBomb(); bombs--; } } } Grid.prototype.XYtoN = function(x,y) { return (y*this.w)+x;//-->n } Grid.prototype.NtoX = function(n) { return n % this.w;//-->x } Grid.prototype.NtoY = function(n) { return Math.floor(n/this.w);//-->y } Grid.prototype.reveal = function(x,y) { var n=this.XYtoN(x,y); this[n].reveal(); } Grid.prototype.revealAll = function() { for (var n=0; n<this.length; n++) { this[n].reveal(true); } } Grid.prototype.flag = function(x,y) { var n=this.XYtoN(x,y); this[n].flag(); } Grid.prototype.checkWin = function() { if (this.flagsAvail==0 && (this.numVisible>= this.length-this.numBombs)) this.youWin(); } Grid.prototype.youWin = function() { alert("You win!"); } Grid.prototype.youLose = function() { alert("KABOOM!"); this.revealAll(); } //--- Cell --- function Cell(n,grid) { if (!grid) return; this.grid = grid; this.n = n; this.x = grid.NtoX(n); this.y = grid.NtoY(n); // this.count = 0; this.isBomb = false; this.isVisible = false; this.isFlag = false; if (gui) this.token=gui.makeCell(grid.token,this,this.x,this.y); } Cell.prototype.neighbors = function() { //returns list of grid positions // four booleans (true if NOT adjacent to respective grid edge): var goW = (this.x>0); var goE = (this.x+1 < this.grid.w); var goN = (this.y>0); var goS = (this.y+1 < this.grid.h); var result=[]; if (goN) { var northN = this.n-this.grid.w; if (goW) result.push(northN-1);//NW result.push(northN);//N if (goE) result.push(northN+1);//NE } if (goW) result.push(this.n-1);//W if (goE) result.push(this.n+1);//E if (goS) { var southN =this.n+this.grid.w; if (goW) result.push(southN-1);//SW result.push(southN);//S if (goE) result.push(southN+1);//SE } return result; } Cell.prototype.placeBomb = function() { this.isBomb = true; // var neighbors = this.neighbors(); // for (var i=0; i<neighbors.length; i++) // this.grid[neighbors[i]].count++; } Cell.prototype.reveal = function(skipCheck) { if (this.isVisible || this.isFlag) return; this.isVisible = true; this.grid.numVisible++; if (!skipCheck && this.isBomb) { if (gui) gui.reveal(this.token,0,true); this.grid.flagsAvail--; this.grid.youLose(); return; } var neighbors = this.neighbors(); var bombCount=0; for (var n=0; n<neighbors.length; n++) { if (this.grid[neighbors[n]].isBomb) bombCount++; } if (gui) gui.reveal(this.token,bombCount,this.isBomb); if (!skipCheck) this.grid.checkWin(); if (bombCount==0) { for (var i=0; i<neighbors.length; i++) this.grid[neighbors[i]].reveal(true); } } Cell.prototype.flag = function() { if (this.isVisible) return; //toggle on or off: if (!this.isFlag) {//no flag if (this.grid.flagsAvail==0) return;//none available this.grid.flagsAvail--; } else {//has flag, remove it this.grid.flagsAvail++; } this.isFlag = !this.isFlag; if (gui) gui.flag(this.token,this.isFlag); this.grid.checkWin(); } //--- WraparoundCell --- function WraparoundCell(n,grid) { Cell.call(this,n,grid); } WraparoundCell.prototype=new Cell(0,null); WraparoundCell.prototype.constructor = WraparoundCell; WraparoundCell.prototype.neighbors = function() { var w=this.grid.w; var len=this.grid.length; //delta added to n to obtain neighbor in each dir: var goW = (this.x>0)?-1:w-1; var goE = (this.x+1 < w)? 1:1-w; var goN = (this.y>0)? -w:len-w; var goS = (this.y+1 < this.grid.h)? w:w-len; var result=[]; var northN =this.n+goN; var southN =this.n+goS; result.push(northN+goW); result.push(northN); result.push(northN+goE); result.push(this.n+goW); result.push(this.n+goE); result.push(southN+goW); result.push(southN); result.push(southN+goE); return result; } // Start: function makeMinefield(w,h,bombs) { window.minefield= new Grid(Cell,w,h,bombs); } function makeWrappingMinefield(w,h,bombs) { window.minefield= new Grid(WraparoundCell,w,h,bombs); }
Interface
The interface module has two parts:
- a file of javascript (minesweeper-gui.js) which manipulates the window's DOM
- a file of CSS rules (minesweeper-gui.css) which control the appearance of various DOM classes
minesweeper-gui.js
var gui = {numgrids:0}; gui.doClick = function(ev) { var isRightClick=false; if ("which" in ev) isRightClick = (ev.which >1); if ("button" in ev) isRightClick = (ev.button >1); if (!isRightClick) isRightClick = ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey; if (isRightClick) ev.target.cell.flag(); else ev.target.cell.reveal(); } gui.ensureRow = function(tableid,y) { var rowid=tableid+'row'+y; var tr = document.getElementById(rowid); if (tr) return tr; //use existing row // no such row; make it: tr = document.createElement("tr"); tr.id=rowid; var table = document.getElementById(tableid); table.appendChild(tr); return tr; } gui.makeGrid = function(gridtoken,w,h) { var newid = "minegrid"+this.numgrids; this.numgrids++; var table = document.createElement("table"); table.id=newid; var space = document.getElementById("gamespace"); space.appendChild(table); return newid; } gui.makeCell = function(tableid,celltoken,x,y) { var tr = this.ensureRow(tableid,y); var td = document.createElement("td"); td.className = "unknown"; td.onclick = this.doClick; tr.appendChild(td); td.cell = celltoken; return td; } gui.reveal = function(td,count,isBomb) { if (isBomb) { td.innerHTML="*"; td.className = "bomb"; } else { td.innerHTML = (count)? count: ""; td.className = "clear count"+count; } } gui.flag = function(td,flagOn) { td.innerHTML = flagOn? "#": ""; }
minesweeper-gui.css
#gamespace table { border:1px solid black; margin:0px; padding:0px; text-align:center; } #gamespace td { height:22px; width:22px; } td.unknown { background:#ddd; } td.bomb { background:red; } td.clear { background:white; } td.count1 { color:blue; } td.count2 { color:orange; } td.count3 { color:red; } td.count4 { color:magenta; } td.count5 { color:brown; }
Running the application
To run this application on your own computer, you'll need to create files containing the code from the boxes above. Using any text editor, in a single directory on your computer, create four empty files with the following names:
- mines7.js
- minesweeper-gui.js
- minesweeper-gui.css
- minesweeper.html
Then copy and paste the code from corresponding box above into each of the files, and save each file. You can change the names if you want, but you'll need to change the corresponding string within "minesweeper.html" to match.
Finally, open the file "minesweeper.html" (or whatever you called it) within any web browser, and the application will run within that window.