/
Agent.js
160 lines (131 loc) · 3.91 KB
/
Agent.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import _ from 'underscore';
import Avatar from './Avatar';
import moment from 'moment';
import log from 'loglevel';
import Util from './Util';
function randomChoice(choices) {
// where `choices` is an
// array of (choice, prob)
var roll = Math.random(),
acc_prob = 0;
// sort by prob
choices = _.sortBy(choices, s => s[1]);
for (var i=0; i < choices.length; i++) {
var choice = choices[i][0],
prob = choices[i][1];
acc_prob += prob;
if (roll <= acc_prob) {
return choice;
}
}
}
function normalize(dist) {
var total = dist.reduce((acc, val) => acc + val, 0);
return dist.map(p => p/total);
}
function positive(vals) {
// i don't think the math quite works here,
// but in order to support negative utilities
// but still interpret them as a probability distribution,
// this makes all values non-negative and ensures no zeros
var min = Math.min(...vals);
return vals.map(v => v + Math.abs(min) + 1);
}
function temperate(dist, temperature) {
// sample with temperature
// lower temp -> less random
var a = dist.map(p => Math.log(p)/temperature);
var exp = a.map(p => Math.exp(p));
var expsum = exp.reduce((acc, val) => acc + val, 0);
return exp.map(a => a/expsum);
}
class Agent {
constructor(state, temperature=0.01) {
this.id = Util.uuid();
this.state = state;
this.temperature = temperature;
this.action = {};
}
// create an avatar for this agent
spawn(world, coord, floor, color=0xffffff) {
this.avatar = new Avatar(world, coord, floor, color);
this.avatar.agent = this;
this.avatar.mesh.agent = this;
}
update(delta) {
// move the avatar,
// if necessary, and update state coord
if (this.avatar) {
this.avatar.update(delta);
this.state.coord = this.avatar.position;
}
// apply `entropy` state update
this.state = this.entropy(this.state);
if (this.available) {
var [action, newState] = this.decide();
this.prev = {
state: this.state,
action: this.action
};
this.action = action;
this.state = this.execute(this.action, this.state);
// just logging stuff
log.info('============');
log.info(this.id);
log.info(action);
this.utility(this.state, null, false, true);
this.lastAction = action;
log.info(this.state);
}
}
decide() {
var actionsStates = this.successors(this.state),
utilities = actionsStates.map(s => this.utility(s[1])),
dist = temperate(normalize(positive(utilities)), this.temperature);
// [(state, prob), ...]
actionsStates = _.zip(actionsStates, dist);
// sort by prob
// just be consistent
return _.sortBy(actionsStates, s => -s[1])[0][0];
// return randomChoice(actionsStates);
}
// compute successor states for possible actions
successors(state) {
var actions = this.actions(state),
successors = actions.map(a => this.successor(a, this.entropy(_.clone(state))));
return _.zip(actions, successors);
}
// VVV IMPLEMENT THESE VVV
// possible actions given a state
actions(state) {
throw 'not implemented';
}
// compute successor state for an action.
// note that the `entropy` state update is pre-applied
// before being passed to this method.
successor(action, state) {
throw 'not implemented';
}
// utility of a state
// this has to be a positive value or 0
utility(state, prevState, expected=true) {
throw 'not implemented';
}
// there may be other processes
// that need to start as a result of an action
execute(action, state) {
throw 'not implemented';
}
// VVV OPTIONALLY IMPLEMENT THESE VVV
// state updates that occur every time step,
// regardless of action or if the agent is `available`
entropy(state) {
return state;
}
// returns whether or not the agent
// is able to decide/change actions.
get available() {
return true;
}
}
export default Agent;