Mängu algoritmid

Mängija1:

car
See brauser ei tunne objekti canvas!
Lae endale uuem brauser!

Mängija2:

car

Player1: computer Player2: computer
Käigul: Player1 Player2
Vali alustaja ja klõpsa 'Start'!

Enamus huvitavaid mänge ja ka majandisprobleeme on keerulised - neile on väga raske leida 'õiget' lahendust. Majanduses maadeldakse (praegu) ülemaailmselt kriiside ja inflatsiooniga ja kellelgi pole raudkindlat valemit/majandusstrateegiat, kuidas neid tulevikus vältida. Ühegi riigi/majanduse eesotsas pole idioodid, vaid selle riigi/rahva parimad ajud, kuid siiski kriisid juhtuvad ja inflatsioon pureb kogu aeg raha väärtust. Majanduse, nagu ka 'kõrgema liiga' mängude (näiteks Go, male jne) tegevuste (game mechanics) tulemus sõltub väga paljudest varem sooritatud tegevustest/otsustustest, paljud tegevused (teiste firmade/riikide tegevus, vastase käigud) pole ette ennustatavad, seega optimaalse tegevuse üks-ühest algoritmi kas pole üldse võimalik luua (pole piisavalt infot, näiteks vastaste tegevuse kohta) või (isegi lõplike mängude - Go, male jne korral) muutub kõigi võimaluste läbiarvutamine praktiliselt võimatuks - kõigi võimalike erinevate malepartiide arv on suurem kui kogu universumi aatomite arv ja isegi vaid kaheksa käigu ette vaatamisel tekkivate variantide arv on suurem kui tähtede arv kogu universumis.

Sellistes situatsioonis on mõistlik otsida mitte täpset tegevusalgoritmi, vaid mingite kriteeriumide põjal loodud üldisemat, heuristilist algoritmi, mis püüab optimiseerida mõnesid selle probleemi/mängu jaoks olulisi parameetreid. Selline on näiteks poliitiliste parteide tegevus - parempoolsed/kapitalistlikumad parteid püüavad optimiseerida kasumit, vasakpoolsed (sotsialistid) püüavad parandada inimeste heaolu.

Sobiva heuristiku - optimeerimise parameetri(te) - leidmine on (tegelikult) veel raskem ülesanne ja see lahendatakse vaid katse/proovi meetodil - kui näiteks partei pääses valitsusse/otsustajaks ja selle poliitika toimis (valiti uuesti), siis pole põhjust poliitikat (oluliselt) muuta. Kuid kui valimistel ei saavutatud edu, on põhjust hakata poliitikat - tegevuse heuristikat - muuta.

Majandustegevuses ja mängudes (lihtsamates) on hästi toimiva heurisika leidmine sageli täiesti võimalik, tuleb vaid rakendada oma tervet mõistust.

Paljusid majandusprobleeme saab uurida/simuleerida mängudena, vaatlemegi lihtsa näitena järgnevat majandusprobleemil põhinevat mängu.

Kaks mängijat (transpordifirmat) tahavad laadida oma autot pakkidega samast kaubalaost. Mõlemale autole mahub täpselt 3 pakki ja autode kandejõud on sama, 15 T. Mängijad peavad autot kasutama maksimaalselt, s.t. laadima sellele täpselt 15T koorma.

Mängijad võtavad kaubalaost vaheldumisi pakke; võetud pakki teine mängija enam ei saa kasutada.

Võidab see, kes suudab esimesena saada 3 pakki, mille kogukaal on 15.

Pakkide asemel võib rääkida vaid numbritest - paki kaalust.

Seisumasin

Peaaegu kõikki mänge on loomulik käsitleda seisumasinatena (lõplike automaatidena), milles mängu reeglitele vastavate mängijate tegevuste/käikude tulemusena seis muutub, kuni saavutatakse kas (mingi) võiduseis või viik.

Selle mängu seisu võib kirjeldada kolme Javascripti muutujaga: vabad (veel valimata) numbrid, mängijate poolt valitud numbrid ja mängija nimi - seda on tarvis mängu tulemuse näitamiseks ekraanil. Mängija nimi ja tema poolt valitud ruudud on mängija omadused, seega mängijad on nende omadustega Javascripti objektid. Kui mängijaks võib määrata ka arvuti, siis tuleb mängijale lisada ka selleks vastav omadus. Tähistades mängijaid player1,player2, võiks mängu algseisu kirjeldada Javascripti muutujaga

		numbers = [8,1,6,3,5,7,4,9,2];
		player1 = {name:'Mängija1', numbers:[], computer:false};
		player2 = {name:'Mängija2', numbers:[], computer:false};

Algseaded

Mängu alustamiseks on tarvis (funktsioon, mis käivitatakse html-dokumendi avamisel):
- tunnistada kanvaa ja luua selle 2D -kontekst
- leida html-dokumendi väljad (div) mängijatele info näitamiseks
- initsialiseerida algseisu kirjeldavad muutujad
- initsialiseerida mängu globaalsed muutujad

- kas mäng on praegu käimas (gameOn - korduva mängu saamiseks tuleb vahepeal mäng peatada)
- luua mängija klikkide tunnistaja
- kui numbreid näidatakse animeeritud ruutude/kastidena, luua need ruudud ja massiiv nende salvestamiseks
-
window.onload = function() {
		canvas = document.getElementById("gamecanvas");
		ctx = canvas.getContext("2d");
		w0 = canvas.width;
		h0 = canvas.height; 
		inf = document.getElementById("inf"); //globaalne info - alustamine, võit jne
		player1_inf = document.getElementById("player1");  //valitud ruudud
		player2_inf = document.getElementById("player2");	
		numbers = [8,1,6,3,5,7,4,9,2];
		player1 = {name:'Mängija1', numbers:[], computer:false};
		player2 = {name:'Mängija2', numbers:[], computer:false};
		sum = 15; //seda võib mängida ka rohkem kui nelja elemendi valikuga, kuid siis on nõutav summa suurem
canvas.addEventListener('mousedown', function(evt) { playerClick(evt); }, false);
Kui kastid ruudu teha animeeritutena (põrkuvad mänguväljaku taustal - ladu) ja on täidetud mingi tekstuuriga, tuleb laadida see tekstuur (bitmap). Bitmap-piltide laadimine on mängist mittesõltuv (asünkroonne protsess, sõltub arvutivõrgust jne) ja pole võimalik teada, millal see lõpeb, sellepärast võib kastide loomise käivitada alles pärast seda, kui see pilt (tekstuur) on laetud. Kuna seda tekstuuri tuleb kasutada korduvalt, on kasulik see salvestada eraldi kanvaal, seega tuleb algväärtustamisfunktsioonile lisada ka selle kanvaa loomine ja kastide loomise käivitamine:
var texture_canvas = document.createElement('canvas');
		var ctx_texture = texture_canvas.getContext('2d');
		texture = new Image();
		texture.src = 'box_54x54.jpg';
		texture.onload = function() {
        ctx_texture.drawImage(texture, 0, 0);
		create();

Funktsioon create loob ruudud, kasutades ruudu loomise konstruktorfunktsiooni:

function create() {
			ruudud = []; //eelmisest mängust jäänud ruudud eemaldatakse
			var k = 0;
			var wr = Math.floor(w0/(o*tegur));
			var hr = Math.floor(h0/(o*tegur));
			for (var i = 0; i < o; i++) {
			for (var j = 0; j < o; j++) {
				var ruut0 = new ruut(i*wr,j*hr,wr,hr,4, numbers[i*o+j]);
				ruudud.push(ruut0);}
			}
		};		

Objektid ruut joonistatakse kiirema laadimise (koduval mängimisel) jaoks samuti eraldi kanvaale - see tuleb luua juba initsialiseerimisfunktsioonis; mälu kokkuhoiuks võib ruudud siia joonistada väiksematena (tegur) :

		//image cache
		canvas0 = document.createElement('canvas');
		ctx0 = canvas0.getContext('2d');
		//canvas0.id     = "VirtualCanvas";
		canvas0.width  = w0;
		canvas0.height = h0;
		tegur = 2; //how many times squares in ctx0 are smaller 
		
Objekti ruut loomise konstruktorfunktsioon lisab objektle rea omadusi (properties) - ruudu asukoht cash-kanvaal x0, y0 ja selle laius-kõrgus w,h (kust seda tuleb kanvaale laadimiseks otsida), sellel näidatav number n ja selle tegelik asukoht x,y (animatsiooni ajal) kanvaal, samuti selle liikumise kiiruse komponendid x,y; (et kast näiks ruumilisena 3D), joonistatakse selle parem-alumine külg paksema-tumedama joonega (varjus); selle joone laius on l.
function ruut(x0,y0,w,h,l,n){
		pFill = ctx0.createPattern(texture, "repeat");
		ctx0.fillStyle = pFill;
		ctx0.fillRect(x0,y0,w,h);
		ctx0.fillStyle = '#422f07';
		ctx0.lineStyle = '#422f07';
		ctx0.lineWidth = l;
		ctx0.moveTo(x0+w-l/2,l/2);
		ctx0.lineTo(x0+w-l/2,y0+h-l/2);
		ctx0.lineTo(x0+l/2,y0+h-l/2);
		ctx0.stroke();
		var fontsize = Math.floor(h/2);
		ctx0.textAlign = "center";
		ctx0.textBaseline = "bottom";
		ctx0.font= fontsize + "px Arial";
		ctx0.fillText(n+'',x0+w/2,y0+h-h/4);
		this.x0 = x0;
		this.y0 = y0;
		this.w = w;
		this.h = h;
		this.n = n;
		var vx0 = 0.5 + Math.random();
		var vy0 = 0.5 + Math.random();
		this.vx = (Math.random()>0.5)?(vx0):(-vx0);
		this.vy = (Math.random()>0.5)?(vy0):(-vx0);
		this.x = this.w + (w0 - 2*this.w)*Math.random();
		this.y = this.h + (h0 - 2*this.h)*Math.random();
		this.draw = function(){
			ctx.drawImage(canvas0,this.x0,this.y0,this.w,this.h,this.x,this.y,this.w, this.h)};
		
		this.move = function(){
			if (this.x + this.vx  < 0 || this.x + this.vx + this.w > w0)
				this.vx *= -1;
			if (this.y + this.vy  < 0 || this.y + this.vy + this.h > h0){
				this.vy *= -1; //põrge äärelt
				}
			
			this.x += this.vx;
			this.y += this.vy;
		}
	 };
.

Käik

Käigul olles valib mängija mingi vaba numbri ja see liigutatakse vabade numbrite massiivist mängija numbrite massiivi. Seejärel kontrollitakse:
- kas mängija numbrite massivis on kolm numbrit, mille summa on 15 - kui on, siis on mängija võitnud; kui pole, siis kontrollitakse
- kas vabade numbrite massivis on veel numbreid; kui pole, on mäng viik.

Mängija 8inimese9 käik algab klikkamisega


Heuristika ja selle andmestruktuurid

Võiduks peab mängija numbrite seas olema kolmik, mille summa on 15. Seega on mängija eesmärk tegelikult sellise kolmiku saamine ja mängijad võistlevad üksteisega just selliste kolmikute nimel. Kui üks mängijatest valib numbri mingist kolmikust, muutub see kolmik vastasmängijale kasutuks. Mängijale on ilmselt kasulik alati valida selline number, mis võtaks vastasmängijalt ära maksimaalse arvu kolmikuid. Kui vabad numbrid on näiteks 1,3,5,7,9, siis nende seas on kaks kolmikut, mis annavad summa 15: [1,5,9],[3,5,7]. Kui mängija valib numbri 5, muudab ta mõlemad kolmikud vastase jaoks kasututeks (ja need muutuvad potentsiaalselt kasulikeks talle - ta võib hiljem valida ülejäänud numbrid. Iga muu numbri valimisel muutub vastase jaoks kasutuks (ja mängija jaoks potentsiaalselt kasulikuks) vaid üks kolmik. Heuristika võib seega olla:
- vali igal sammul number, mis esineb kõige sagedamini kõigis kolmikutes, milles veel pole vastase numbreid.

Ilmselt on kasulik arvutada mängu algul välja kõik kolmikud; defineerime muutuja vectors ja lisame funktsiooni, mis salvestab selles kõik kolmikud, mille summa on 15 :

vectors = [];
function calc_vectors(){
		//arvutab kõik vektorid pikkusega 3 mille komponentide summa on sum (=15)
		var n = state.numbers.length; 
		var vect = [];
		for(var i = 0; i < n-2;i++){
			vect[0] = state.numbers[i];
			for(var j = i+1; j < n-1;j++){
				vect[1]=state.numbers[j];
				for(var k=j+1; k < n;k++){
				if (vect[0] + vect[1] + state.numbers[k]==sum){
					vect[2] = state.numbers[k];
					vectors.push(vect.slice(0));
					//console.log(vect,vectors);} //vt miks slice(0) !
				}
				}
			}
		}

Kuna Javascriptis omistatakse massiive/vektoreid viitega (by reference) ja mitte massiivist uue koopia tegemisega, ei saa kasutada leitud kolmiku vect lisamiseks kolmikute massiivi vectors loomulikuna näivat vectors.push(vect); (või veidi halvemat omistamist vectors[l] = vect - selle kasutamisel peab programmeerija tagama indeksi l õige väärtuse) - massiivile vect uue väärtuse andmisel muutuvad ka kõik juba massivi vectors lisatud väärtused (need on viited, mitte koopiad!) ja tulemusena oleks kõik massivi vectors elemendid samad - need on kõik viited massiivile vect, mitte selle koopiad! Uue koopia massiivist teeb funktsioon slice(0); selle mõju saab Firebug-is või Chrome's jälgida, kui eemaldada kommentaarimärgid.

Kuna mängijale kasulikud kolmikud (sellised, kus vastasmängija pole veel valinud ühtegi numbrit) on käigu arvutamisel vajalik info, lisame need ka seisu kirjeldusele. Mängu algul on kõik kolmikud mõlemale kasulikud mängijale, seega täiendame mängu algseisu kirjeldust (kuna need massiivid hakkavad muutuma sõltumatult, tuleb taas luua massivi koopia, mitte viide!) :

		state.p1_vectors = vectors.slice(0);
		state.p2_vectors = vectors.slice(0);
Kui mängija p1 valib objekti numbriga n, tuleb see eemaldada vastasmängija jaoks sobivate objektide nimekirjast:
function remove_vectors(player,n){
		var nm = player+"_vectors";
		var kolmikud = state[nm];
		for(var i = kolmikud.length -1;i >= 0;i--){
			if (kolmikud[i].indexOf(n) > -1)
				kolmikud.splice(i,1);
		}
	}

Siin oli näide, kuidas viidata Javasripti objekti state omadusele (p1_vectors, p2_vectors), kasutades muutujat (teine võimalus - if-lause muutuja player väärtuse järgi). Kui Javascripti objekti omadusele tahetakse viidata muutuja abil, tuleb kasutada massiivitähistust: state[nm], tavalise punktiga tähistuse korral state.nm loeb Javascript nm-i attribuudiks.

Seega võib käigu kirjeldada Javascripti funktsiooniga



Kasutajaliides

Selles mängus on kolm olulist komponenti: laos olevad pakid ja kummagi mängija poolt juba valitud pakid. Kui realiseerida paki valimine pakil klõpsamisega (võik ka näiteks klaviatuurilt), siis on loomulik realiseerida kasutajaliides kolmeveerulisena: keskel on ladu, sellest mõlemal pool mängijate (teine mängija võib olla ka arvuti) valitud pakid; nende kolme veeru all on valikud (alustaja, kas mängija on arvuti, info jne).

Kolmeveerulise paigutuse saamine CSS-ga on lihtne. Algul jagame kasutajaliidese ala kaheks kastiks (DIV-iks), esimene on veetud vasakule, s.t. attribuudiga float:left, teine paremale attribuudiga float:right, seejärel jagame parempoolse kasti veel kord, kasutades samu attribuute - tulemuseks ongi kolmeveeruline paigutus.

Et veerud ja nende alla paigutatud info-kast koos püsiks, on kõige ümber veel üks kast, mille abil saab määrata kogu liidese paigutuse html-lehel.


Ülesandeid

1. Teine versioon: kahe mängija vahel jagatakse täisarvud 1..9, ühele - paarisarvud, teisele - paaritud. Mängijad kordamööda lisavad mängulauale mingi oma arvu (sama arvu korrata ei saa); võidab see, kes lisab arvu, mille abil saab juba lisatud arvudest moodustada summa 15.