First draft
authorBenjamin Auder <benjamin.auder@somewhere>
Wed, 24 Jan 2018 02:06:27 +0000 (03:06 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Wed, 24 Jan 2018 02:06:27 +0000 (03:06 +0100)
index.html [new file with mode: 0644]
rpsls.js [new file with mode: 0644]

diff --git a/index.html b/index.html
new file mode 100644 (file)
index 0000000..31303f9
--- /dev/null
@@ -0,0 +1,15 @@
+<!-- TODO: click : on image (show computer move on same image) + show network-matrix on the right (top-bottom, 5 per row) -->
+
+<div id="rpsls">
+       <input id="drawIsLost" type="checkbox" v-model="drawIsLost">
+       <label for="drawIsLost"> Draw count as loss ?</label>
+       <input id="nInput" type="number" v-model="nInput" @change="reinitialize()">
+       <label for="nInput">Number of inputs</label>
+       <input id="humanMove" type="number" v-model="humanMove" @change="play()">
+       <label for="humanMove">Move ?</label>
+       <input id="gameState" type="number" v-model="gameState" disabled>
+       <label for="gameState">Score of computer</label>
+</div>
+
+<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js">
+<script src="rpsls.js">
diff --git a/rpsls.js b/rpsls.js
new file mode 100644 (file)
index 0000000..42c87b6
--- /dev/null
+++ b/rpsls.js
@@ -0,0 +1,103 @@
+const nChoice = 5,; //fixed for this game\r
+\r
+// Rewards matrix. Order: rock, lizard, Spock, scissors, paper\r
+const rewards = Array.from(Array(nChoice)).map( i => { //lines\r
+       return Array.from(Array(nChoice)).map( j => { //columns\r
+               // i against j: gain from i viewpoint\r
+               if (j == (i+1) % nChoice || j == (i+3) % nChoice)\r
+                       return 1; //I win :)\r
+               else if ((i+1) % nChoice == j || j == (i+2) % nChoice)\r
+                       return -1; //I lose :(\r
+               else\r
+                       return 0;\r
+       });\r
+});\r
+\r
+const symbols = [ "Rock", "Lizard", "Spock", "Scissors", "Paper" ];\r
+\r
+new Vue({\r
+       el: "#rpsls",\r
+       data: {\r
+               humanMove: -1, //integer in 0...nChoice-1\r
+               nInput: 5, //default\r
+               humanHistory: [ ], //your nInput last moves, stored (oldest first)\r
+               gameState: 0, //total points of the computer\r
+               drawIsLost: false, //normal (or true: draw is considered loss)\r
+               rewards: [ ], //initialized at first human move with new nInput\r
+               weights: [ ], //same as above\r
+       },\r
+       methods: {\r
+               // Called on nInput change\r
+               reinitialize: function() {\r
+                       // weights[i][j][k]: output i -- input j/choice k\r
+                       this.weights = Array.from(Array(nChoice)).map( i => {\r
+                               return Array.from(Array(this.nInput)).map( j => {\r
+                                       return Array.from(Array(nChoice)).map( k => {\r
+                                               return 0;\r
+                                       });\r
+                               })\r
+                       });\r
+                       if (this.humanHistory.length > this.nInput)\r
+                               this.humanHistory.splice(this.nInput);\r
+               },\r
+               // Play a move given current data (*after* human move: trigger onChange)\r
+               play: function() {\r
+                       let candidates = [ ];\r
+                       Array.from(Array(nChoice)).forEach( i => {\r
+                               // Sum all weights from an activated input to this output\r
+                               let sumWeights = this.weights[i].reduce( (acc,input,j) => {\r
+                                       if (this.humanHistory.length <= j)\r
+                                               return 0;\r
+                                       return input[ this.humanHistory[j] ];\r
+                               }, 0 );\r
+                               let currentValue = {\r
+                                       val: sumWeights,\r
+                                       index: i\r
+                               };\r
+                               if (candidates.length == 0 || sumWeights > candidates[0].val)\r
+                                       candidates = [ currentValue ];\r
+                               else if (sumWeights == candidates[0].val)\r
+                                       candidates.push(currentValue);\r
+                       });\r
+                       // Pick a choice at random in maxValue (total random for first move)\r
+                       let randIdx = Math.floor((Math.random() * candidates.length) + 1);\r
+                       this.updateGameState(candidates[randIdx]);\r
+               },\r
+               updateGameState: function(move) {\r
+                       let reward = rewards[move.val][this.humanMove]; //viewpoint of computer\r
+                       this.gameState += reward;\r
+                       this.updateWeights(reward, move.index);\r
+               },\r
+               updateWeights: function(reward, index) {\r
+                       let delta = Math.sign(reward);\r
+                       if (this.drawIsLost && reward == 0)\r
+                               delta = -1;\r
+                       this.weights[index].forEach( (input,i) => {\r
+                               if (i < this.humanHistory.length)\r
+                                       input[ this.humanHistory[i] ] += delta;\r
+                       });\r
+                       this.postUpdate();\r
+               },\r
+               // Finalize weights update\r
+               postUpdate: function() {\r
+                       // Re-center the weights\r
+                       let sumAllWeights = this.weights.reduce( (a,output) => {\r
+                               return a + output.reduce( (b,input) => {\r
+                                       return b + input.reduce( (c,choiceWeight) => {\r
+                                               return c + choiceWeight;\r
+                                       }, 0);\r
+                               }, 0);\r
+                       }, 0);\r
+                       let meanWeight = sumAllWeights / (this.nInput * nChoice * nChoice);\r
+                       this.weights.forEach( output => {\r
+                               output.forEach( input => {\r
+                                       for (let i=0; i<input.length; i++)\r
+                                               input[i] -= meanWeight;\r
+                               });\r
+                       });\r
+                       // Update human moves history\r
+                       this.humanHistory.push(coup_adversaire);\r
+                       this.humanHistory.shift();\r
+               },\r
+  },\r
+};\r