X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=js%2Frpsls.js;fp=js%2Frpsls.js;h=cff3001a3403f0574a4da691cb7ae7232bc19f36;hb=d666e77138bb5e3e397d68dbbddc28845c81068b;hp=0000000000000000000000000000000000000000;hpb=932d367fbda2dacd73da177b54f0ec00489225b4;p=rpsls-bot.git diff --git a/js/rpsls.js b/js/rpsls.js new file mode 100644 index 0000000..cff3001 --- /dev/null +++ b/js/rpsls.js @@ -0,0 +1,117 @@ +const nChoice = 5; //fixed for this game + +const symbols = [ "Rock", "Lizard", "Spock", "Scissors", "Paper" ]; + +// Same order as in symbols +const messages = [ + [ "=", "crushes", "is vaporized by", "cruches", "is covered by" ], + [ "is crushed by", "=", "poisons", "is decapitated by", "eats" ], + [ "vaporizes", "is poisoned by", "=", "smashes", "is disproved by" ], + [ "is crushed by", "decapitates", "is smashed by", "=", "cuts" ], + [ "covers", "is eaten by", "disproves", "is cut by", "=" ], +]; + +// Rewards matrix, order as in symbols +const rewards = Array.from(Array(nChoice)).map( (e,i) => { //lines + return Array.from(Array(nChoice)).map( (f,j) => { //columns + // i against j: gain from i viewpoint + if (j == (i+1) % nChoice || j == (i+3) % nChoice) + return 1; //I win :) + else if (i != j) + return -1; //I lose :( + return 0; //i==j + }); +}); + +new Vue({ + el: "#rpsls", + data: { + nInput: 5, //default + humanHistory: [ ], //your nInput last moves, stored (oldest first) + humanMove: -1, + gameState: 0, //total points of the human + compMove: -1, //last move played by computer + drawIsLost: false, //normal (or true: draw is considered loss) + rewards: [ ], //initialized at first human move with new nInput + weights: [ ], //same as above + symbols: symbols, + messages: messages, + }, + created: function() { + this.reinitialize(); + }, + methods: { + // Called on nInput change + reinitialize: function() { + // weights[i][j][k]: output i -- input j/choice k + this.weights = Array.from(Array(nChoice)).map( i => { + return Array.from(Array(this.nInput)).map( j => { + return Array.from(Array(nChoice)).map( k => { + return 0; + }); + }) + }); + if (this.humanHistory.length > this.nInput) + this.humanHistory.splice(this.nInput); + }, + // Play a move given current data (*after* human move) + play: function(humanMove) { + this.humanMove = humanMove; + let candidates = [ ]; + Array.from(Array(nChoice)).forEach( (e,i) => { + // Sum all weights from an activated input to this output + let sumWeights = this.weights[i].reduce( (acc,input,j) => { + if (this.humanHistory.length <= j) + return 0; + return input[ this.humanHistory[j] ]; + }, 0 ); + let move = { + val: sumWeights, + index: i, + }; + if (candidates.length == 0 || sumWeights > candidates[0].val) + candidates = [move]; + else if (sumWeights == candidates[0].val) + candidates.push(move); + }); + // Pick a choice at random in maxValue (total random for first move) + let randIdx = Math.floor(Math.random() * candidates.length); + this.compMove = candidates[randIdx].index; + // Update game state + let reward = rewards[this.compMove][humanMove]; //viewpoint of computer + this.gameState -= reward; + this.updateWeights(reward); + // Update human moves history + this.humanHistory.push(humanMove); + if (this.humanHistory.length > this.nInput) + this.humanHistory.shift(); + }, + updateWeights: function(reward) { + let delta = Math.sign(reward); + if (this.drawIsLost && reward == 0) + delta = -1; + this.weights[this.compMove].forEach( (input,i) => { + if (i < this.humanHistory.length) + input[ this.humanHistory[i] ] += delta; + }); + // Re-center the weights + let sumAllWeights = this.weights.reduce( (a,output) => { + return a + output.reduce( (b,input) => { + return b + input.reduce( (c,choiceWeight) => { + return c + choiceWeight; + }, 0); + }, 0); + }, 0); + let meanWeight = sumAllWeights / (this.nInput * nChoice * nChoice); + this.weights.forEach( output => { + output.forEach( input => { + for (let i=0; i