Better messages in case of draw
[rpsls-bot.git] / js / rpsls.js
1 const nChoice = 5; //fixed for this game
2
3 const symbols = [ "Rock", "Lizard", "Spock", "Scissors", "Paper" ];
4
5 // Same order as in symbols
6 const messages = [
7 [ "can't win vs.", "crushes", "is vaporized by", "cruches", "is covered by" ],
8 [ "is crushed by", "can't win vs.", "poisons", "is decapitated by", "eats" ],
9 [ "vaporizes", "is poisoned by", "can't win vs.", "smashes", "is disproved by" ],
10 [ "is crushed by", "decapitates", "is smashed by", "can't win vs.", "cuts" ],
11 [ "covers", "is eaten by", "disproves", "is cut by", "can't win vs." ],
12 ];
13
14 // Rewards matrix, order as in symbols
15 const rewards = Array.from(Array(nChoice)).map( (e,i) => { //lines
16 return Array.from(Array(nChoice)).map( (f,j) => { //columns
17 // i against j: gain from i viewpoint
18 if (j == (i+1) % nChoice || j == (i+3) % nChoice)
19 return 1; //I win :)
20 else if (i != j)
21 return -1; //I lose :(
22 return 0; //i==j
23 });
24 });
25
26 new Vue({
27 el: "#rpsls",
28 data: {
29 nInput: 5, //default
30 humanHistory: [ ], //your nInput last moves, stored (oldest first)
31 humanMove: -1,
32 gameState: 0, //total points of the human
33 compMove: -1, //last move played by computer
34 drawIsLost: false, //normal (or true: draw is considered loss)
35 rewards: [ ], //initialized at first human move with new nInput
36 weights: [ ], //same as above
37 symbols: symbols,
38 messages: messages,
39 },
40 created: function() {
41 this.reinitialize();
42 },
43 methods: {
44 // Called on nInput change
45 reinitialize: function() {
46 // weights[i][j][k]: output i -- input j/choice k
47 this.weights = Array.from(Array(nChoice)).map( i => {
48 return Array.from(Array(this.nInput)).map( j => {
49 return Array.from(Array(nChoice)).map( k => {
50 return 0;
51 });
52 })
53 });
54 if (this.humanHistory.length > this.nInput)
55 this.humanHistory.splice(this.nInput);
56 },
57 // Play a move given current data (*after* human move)
58 play: function(humanMove) {
59 this.humanMove = humanMove;
60 let candidates = [ ];
61 Array.from(Array(nChoice)).forEach( (e,i) => {
62 // Sum all weights from an activated input to this output
63 let sumWeights = this.weights[i].reduce( (acc,input,j) => {
64 if (this.humanHistory.length <= j)
65 return 0;
66 return input[ this.humanHistory[j] ];
67 }, 0 );
68 let move = {
69 val: sumWeights,
70 index: i,
71 };
72 if (candidates.length == 0 || sumWeights > candidates[0].val)
73 candidates = [move];
74 else if (sumWeights == candidates[0].val)
75 candidates.push(move);
76 });
77 // Pick a choice at random in maxValue (total random for first move)
78 let randIdx = Math.floor(Math.random() * candidates.length);
79 this.compMove = candidates[randIdx].index;
80 // Update game state
81 let reward = rewards[this.compMove][humanMove]; //viewpoint of computer
82 this.gameState -= reward;
83 this.updateWeights(reward);
84 // Update human moves history
85 this.humanHistory.push(humanMove);
86 if (this.humanHistory.length > this.nInput)
87 this.humanHistory.shift();
88 },
89 updateWeights: function(reward) {
90 let delta = Math.sign(reward);
91 if (this.drawIsLost && reward == 0)
92 delta = -1;
93 this.weights[this.compMove].forEach( (input,i) => {
94 if (i < this.humanHistory.length)
95 input[ this.humanHistory[i] ] += delta;
96 });
97 // Re-center the weights
98 let sumAllWeights = this.weights.reduce( (a,output) => {
99 return a + output.reduce( (b,input) => {
100 return b + input.reduce( (c,choiceWeight) => {
101 return c + choiceWeight;
102 }, 0);
103 }, 0);
104 }, 0);
105 let meanWeight = sumAllWeights / (this.nInput * nChoice * nChoice);
106 this.weights.forEach( output => {
107 output.forEach( input => {
108 for (let i=0; i<input.length; i++)
109 input[i] -= meanWeight;
110 });
111 });
112 },
113 imgSource: function(symbol) {
114 return "img/" + symbol + ".png";
115 },
116 },
117 });