Skip to content

Commit f3f426c

Browse files
committed
Add test harness and payoff display + calculation.
1 parent 88c2db7 commit f3f426c

File tree

5 files changed

+2643
-2330
lines changed

5 files changed

+2643
-2330
lines changed

acsg.js

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/* jshint esversion: 6, asi: true */
2+
/* globals require */
3+
14
var util = require('util')
25
var css = require('dom-css')
36
var fs = require('fs')
@@ -43,6 +46,20 @@ function filenameFrom(data) {
4346
return experimentID + '-decompressed.json'
4447
}
4548

49+
function sum(vector) {
50+
return vector.reduce(( accumulator, currentValue ) => accumulator + currentValue, 0);
51+
}
52+
53+
function softmax(vector, temperature=1){
54+
/* The softmax activation function. */
55+
var new_vector = vector.map(x => Math.pow(x, temperature));
56+
if (sum(new_vector)) {
57+
return new_vector.map(x => x / sum(new_vector));
58+
} else {
59+
return new_vector.map(_ => vector.length);
60+
}
61+
}
62+
4663
var acsg = {} // Module namespace
4764

4865
acsg.Browser = (function () {
@@ -56,6 +73,7 @@ acsg.Browser = (function () {
5673
var backgroundRngFunc = seedrandom(this.now())
5774
this.rBackground = new Rands(backgroundRngFunc)
5875
this.scoreboard = document.getElementById('score')
76+
this.bonus = document.getElementById('dollars')
5977
this.clock = document.getElementById('clock')
6078
this.data = []
6179
this.background = []
@@ -87,8 +105,9 @@ acsg.Browser = (function () {
87105
return performance.now()
88106
}
89107

90-
Browser.prototype.updateScoreboard = function (score) {
91-
this.scoreboard.innerHTML = score
108+
Browser.prototype.updateScoreboard = function (ego) {
109+
this.scoreboard.innerHTML = ego.score
110+
this.bonus.innerHTML = ego.payoff.toFixed(2)
92111
}
93112

94113
Browser.prototype.updateClock = function (t) {
@@ -229,7 +248,7 @@ acsg.CLI = (function () {
229248
// Noop
230249
}
231250

232-
CLI.prototype.updateScoreboard = function (score) {
251+
CLI.prototype.updateScoreboard = function (ego) {
233252
// Noop
234253
}
235254

@@ -409,6 +428,7 @@ acsg.Player = (function () {
409428
this.teamIdx = Math.floor(Math.random() * teamColors.length)
410429
this.color = config.color || teamColors[this.teamIdx]
411430
this.score = config.score || 0
431+
this.payoff = 0
412432

413433
return this
414434
}
@@ -557,6 +577,9 @@ acsg.Game = (function () {
557577
this.opts.BLOCK_SIZE = opts.BLOCK_SIZE || 15
558578
this.opts.BLOCK_PADDING = opts.BLOCK_PADDING || 1
559579
this.opts.BOT_STRATEGY = opts.BOT_STRATEGY || 'random'
580+
this.opts.INTERGROUP_COMPETITION = opts.INTERGROUP_COMPETITION || 1
581+
this.opts.INTRAGROUP_COMPETITION = opts.INTRAGROUP_COMPETITION || 1
582+
this.opts.DOLLARS_PER_POINT = opts.DOLLARS_PER_POINT || 0.02
560583
this.UUID = uuidv4()
561584
this.replay = false
562585
this.humanActions = []
@@ -604,14 +627,19 @@ acsg.Game = (function () {
604627
}
605628

606629
Game.prototype.serializeActions = function () {
607-
return JSON.stringify({
630+
var data = {
608631
'id': this.UUID,
609632
'data': {
610633
'actions': this.humanActions,
611634
'timestamps': this.humanActionTimestamps
612635
},
613636
'config': opts
614-
})
637+
}
638+
if (this.opts.INCLUDE_HUMAN && this.world.players && this.world.players[0]) {
639+
data.data.score = this.world.players[0].score
640+
data.data.payoff = this.world.players[0].payoff.toFixed(2)
641+
}
642+
return JSON.stringify(data)
615643
}
616644

617645
Game.prototype.serializeFullState = function () {
@@ -665,6 +693,58 @@ acsg.Game = (function () {
665693
}
666694
}
667695

696+
Game.prototype.computePayoffs = function () {
697+
/* Compute payoffs from scores.
698+
699+
A player's payoff in the game can be expressed as the product of four
700+
factors: the grand total number of points earned by all players, the
701+
(softmax) proportion of the total points earned by the player's group,
702+
the (softmax) proportion of the group's points earned by the player,
703+
and the number of dollars per point.
704+
705+
Softmaxing the two proportions implements intragroup and intergroup
706+
competition. When the parameters are 1, payoff is proportional to what
707+
was scored and so there is no extrinsic competition. Increasing the
708+
temperature introduces competition. For example, at 2, a pair of groups
709+
that score in a 2:1 ratio will get payoff in a 4:1 ratio, and therefore
710+
it pays to be in the highest-scoring group. The same logic applies to
711+
intragroup competition: when the temperature is 2, a pair of players
712+
within a group that score in a 2:1 ratio will get payoff in a 4:1
713+
ratio, and therefore it pays to be a group's highest-scoring member. */
714+
var group_info, group_scores, ingroup_players,
715+
ingroup_scores, intra_proportions, inter_proportions,
716+
p, i;
717+
var player_groups = {}
718+
var total_payoff = 0
719+
var player = this.world.players[0]
720+
721+
for (i = 0; i < this.world.players.length; i++) {
722+
p = this.world.players[i]
723+
group_info = player_groups[p.teamIdx]
724+
if (group_info === undefined) {
725+
player_groups[p.teamIdx] = group_info = {players: [], scores: [], total: 0}
726+
}
727+
group_info.players.push(p)
728+
group_info.scores.push(p.score)
729+
group_info.total += p.score
730+
total_payoff += p.score
731+
}
732+
group_scores = Object.values(player_groups).map(g => g.total);
733+
group_info = player_groups[player.teamIdx];
734+
ingroup_players = group_info.players
735+
ingroup_scores = group_info.scores
736+
intra_proportions = softmax(
737+
ingroup_scores, this.opts.INTRAGROUP_COMPETITION
738+
);
739+
player.payoff = total_payoff * intra_proportions[0]
740+
741+
inter_proportions = softmax(
742+
group_scores, this.opts.INTERGROUP_COMPETITION
743+
);
744+
player.payoff *= inter_proportions[player.teamIdx]
745+
player.payoff *= this.opts.DOLLARS_PER_POINT
746+
}
747+
668748
Game.prototype.run = function (callback) {
669749
var self = this
670750
var callback = callback || function () { console.log('Game finished.') }
@@ -701,14 +781,17 @@ acsg.Game = (function () {
701781
// Carry out human action.
702782
lastHumanActionIdx += 1
703783
ego.move(self.humanActions[lastHumanActionIdx])
704-
self.ui.updateScoreboard(ego.consume())
784+
ego.consume()
785+
self.ui.updateScoreboard(ego)
705786
self.world.recordStateAt(nextHumanT)
706787
}
788+
self.computePayoffs()
707789
}
708790

709791
self.ui.updateGrid(self.world)
710792
self.ui.updateClock(self.opts.DURATION - elapsedTime)
711793

794+
self.computePayoffs()
712795
if (lastBotActionIdx >= botMotion.botIds.length - 1) {
713796
if (!self.gameOver) {
714797
self.gameOver = true

acsg.test.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/* jshint esversion: 6 */
2+
/* globals require,test,expect,afterEach,beforeEach,jest,console */
3+
4+
const rewire = require('rewire');
5+
const acsg = require('./acsg');
6+
// Access private functions for unit tests
7+
const acsgRewire = rewire('./acsg');
8+
var outputData = '';
9+
var storeLog = inputs => (outputData += inputs);
10+
console.log = jest.fn(storeLog);
11+
12+
// Random seed
13+
beforeEach(() => {
14+
outputData = '';
15+
});
16+
17+
// Generate basic world
18+
function init_game(config) {
19+
var game_config = {SEED: 2, INCLUDE_HUMAN: true, IS_CLI: true};
20+
Object.assign(game_config, config);
21+
var game = acsg.Game({config: game_config});
22+
return game;
23+
}
24+
25+
test('sum function', () => {
26+
var sum = acsgRewire.__get__('sum');
27+
expect(sum([1, 2, 3])).toEqual(6);
28+
expect(sum([8, 9, 10])).toEqual(27);
29+
expect(sum([])).toEqual(0);
30+
});
31+
32+
test('softmax function', () => {
33+
const softmax = acsgRewire.__get__('softmax');
34+
expect(softmax([1, 2, 3])).toEqual([1/6, 1/3, 1/2]);
35+
expect(softmax([0, 0, 0])).toEqual([3, 3, 3]);
36+
expect(softmax([1, 2, 3], 2)).toEqual([1/14, 2/7, 9/14]);
37+
});
38+
39+
test('player move changes position', () => {
40+
var game = init_game();
41+
// Create a player
42+
var player = game.world.players[0];
43+
// initial position is random, but deterministic based on seed
44+
expect(player.position).toEqual([9, 15]);
45+
// move() returns direction
46+
expect(player.move('up')).toEqual('up');
47+
expect(player.position).toEqual([8, 15]);
48+
player.move('down');
49+
expect(player.position).toEqual([9, 15]);
50+
player.move('left');
51+
expect(player.position).toEqual([9, 14]);
52+
player.move('right');
53+
expect(player.position).toEqual([9, 15]);
54+
// Nonsense directions are logged, and do nothing;
55+
player.move('nonsense');
56+
expect(outputData).toEqual('Direction not recognized.');
57+
expect(player.position).toEqual([9, 15]);
58+
});
59+
60+
test('calculate straight payoff', () => {
61+
var game = init_game({
62+
NUM_PLAYERS: 2,
63+
DOLLARS_PER_POINT: 0.05
64+
});
65+
var player = game.world.players[0];
66+
// Payoff is a simple multiple of our score
67+
player.score = 10;
68+
expect(player.payoff).toEqual(0);
69+
game.computePayoffs();
70+
expect(player.payoff).toEqual(0.5);
71+
player.score = 20;
72+
game.computePayoffs();
73+
expect(player.payoff).toEqual(1);
74+
player.score = 0;
75+
game.computePayoffs();
76+
expect(player.payoff).toEqual(0);
77+
// Other player scores don't matter
78+
player.score = 20;
79+
game.world.players[1].score = 50;
80+
game.computePayoffs();
81+
expect(player.payoff).toEqual(1);
82+
83+
});
84+
85+
test('calculate intragroup payoff', () => {
86+
var game = init_game({
87+
NUM_PLAYERS: 2,
88+
DOLLARS_PER_POINT: 0.05,
89+
INTRAGROUP_COMPETITION: 2
90+
});
91+
var player = game.world.players[0];
92+
var bot1 = game.world.players[1];
93+
expect(player.teamIdx).toEqual(0);
94+
expect(bot1.teamIdx).toEqual(0);
95+
// We get all the payoff if we get all the points
96+
player.score = 20;
97+
bot1.score = 0;
98+
game.computePayoffs();
99+
expect(player.payoff).toEqual(1);
100+
// We get a payoff based on our portion of the in-group total
101+
bot1.score = 10;
102+
game.computePayoffs();
103+
expect(player.payoff).toEqual(30 * 4/5 * 0.05);
104+
});
105+
106+
test('calculate intergroup payoff', () => {
107+
var game = init_game({
108+
NUM_PLAYERS: 4,
109+
DOLLARS_PER_POINT: 0.1,
110+
INTERGROUP_COMPETITION: 2
111+
});
112+
var player = game.world.players[0];
113+
var bot1 = game.world.players[1];
114+
var bot2 = game.world.players[2];
115+
var bot3 = game.world.players[3];
116+
expect(player.teamIdx).toEqual(0);
117+
expect(bot1.teamIdx).toEqual(0);
118+
expect(bot2.teamIdx).toEqual(1);
119+
expect(bot3.teamIdx).toEqual(1);
120+
// Point distribution between groups determines payoff
121+
player.score = 20;
122+
bot1.score = 0;
123+
bot2.score = 20;
124+
bot3.score = 40;
125+
game.computePayoffs();
126+
expect(player.payoff).toEqual(80 * 1/10 * 0.1);
127+
// Point distribution within our group changes the payoff
128+
player.score = 10;
129+
bot1.score = 10;
130+
game.computePayoffs();
131+
expect(player.payoff).toEqual(80 * 1/10 * 0.1 * 0.5);
132+
// Point distribution in the out-group has no impact
133+
player.score = 20;
134+
bot1.score = 0;
135+
bot2.score = 10;
136+
bot3.score = 50;
137+
game.computePayoffs();
138+
expect(player.payoff).toEqual(80 * 1/10 * 0.1);
139+
});

0 commit comments

Comments
 (0)