├── .gitignore
├── LICENSE
├── README.md
├── _config.yml
├── ballstate.js
├── bitmanager.js
├── canvaswrapper.js
├── imports
└── lodash.js
├── index.html
├── keyboard.js
├── main.css
├── main.js
├── notes.txt
├── paddlestate.js
├── pong.js
├── scoreboard.js
└── util.js
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.*.swp
2 | tags
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Eric Uhrhane
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CellCulTuring
2 | This is a [cellular automaton](https://en.wikipedia.org/wiki/Cellular_automaton) that
3 | implements [Pong](https://en.wikipedia.org/wiki/Pong).
4 |
5 | You can play it [here](https://ericu.github.io/CellCulTuring/) and see the code [here](https://github.com/ericu/CellCulTuring).
6 |
7 | # Q&A
8 | * Why would you want to write a video game as a cellular automaton?
9 |
10 | You wouldn't. It's inefficient, overly constrained, tedious, and fun.
11 |
12 | * How does it work?
13 |
14 | As in cellular automata such as [Conway's Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life), at each generation [each screen refresh] the next state of each pixel is determined only by the current state of that pixel and those of its 8 immediate neighbors. However, whereas Life has only 2 states--black and white--this Pong uses 32-bit colors; and whereas Life's rules can be written in 4 lines, Pong's took a couple of thousand lines of JavaScript to express. If you want to get an idea of what it's doing, click "Use more revealing colors". That doesn't change how the game works; it just rearranges how the color bits are allocated, so that certain interesting values show up in high-order bits of the color components.
15 |
16 | * How many states does it have?
17 |
18 | I haven't counted them, but it uses all 32 bits in various combinations, and there's not a lot of storage space wasted. I believe it's safe to say that there are millions of valid states. For example, the motion of the ball is described by 8 bits, to capture the 30 angles at which it can travel and the state involved in animating that motion. And that ball can be traveling through regions of the board that hold other state, so the ball color needs to include those bits as it travels through them.
19 |
20 | * How is this different from running a million copies of a video game and letting each control one display pixel?
21 |
22 | In that case, you'd have no dependency on your neighbors' states, and you'd have to store the entire game's state a million times. In this case, the game's state is distributed across all the pixels, stored just in the colors themselves. There is no hidden state, assuming your eyes can distinguish single-low-bit differences between colors. No pixel knows anything about what's going on elsewhere on the board.
23 |
24 | * Then how does user input work?
25 |
26 | It doesn't, really. This is really only a true cellular automaton when it's playing against itself. But I couldn't very well publish a game that nobody could play, so I cheated. In addition to the state of its neighbors, each cell also has access to the keyboard state. If it makes you feel better, imagine a plane behind the image whose color is controlled by user input, so every cell has just one extra neighbor.
27 |
28 | * So you read and write the state right from the canvas?
29 |
30 | No, that doesn't work. If you write a value to the HTML5 canvas and read it back, you're [not guaranteed to get the same value back](https://stackoverflow.com/questions/23497925/how-can-i-stop-the-alpha-premultiplication-with-canvas-imagedata/23501676#23501676). In my experience, there's often a bit of rounding going on, which isn't a big deal for graphics, but kills you if your colors are sets of bitflags. I use an offscreen [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) and blit it to the canvas once per frame.
31 |
32 | * How does the computer's paddle know where to go to hit the ball?
33 |
34 | When a paddle hits the ball, it causes the creation of a wave of color that sweeps across to the other paddle. That wave is a message telling the paddle where to expect the ball. Since the ball's path is deterministic, I can compute that from its angle and the board's dimensions. I made the message move across as fast as possible, 1 pixel per cycle. I made the ball move at half that speed, to give the message time to get there while the paddle could still do something about it. At the current board size, it makes for a good-but-not-unbeatable opponent. If the board were twice as wide as it is high, the computer would never miss.
35 |
36 | * Is the scoreboard part of the automaton?
37 |
38 | Yes, everything inside the canvas is part of it, and is running under the same set of rules.
39 |
40 | * How does the ball travel at angles other than 45°?
41 |
42 | I use [Bresenham's algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm).
43 |
44 | * What was the most complex feature to implement?
45 |
46 | Handling a ball more than 1 pixel across. It's tricky to make the left side of the ball know when the right side hits the paddle, for example.
47 |
48 | * How does that work?
49 |
50 | Anywhere the ball needs to bounce, there's a buffer region the same width as the ball with special marking and behavior. As the ball travels into the buffer, it keeps track of how deep into it it's gone. There are some tricks by which the pixels tell their neighbors about the current depth and we keep track of whether we're hitting or missing the paddle, but basically you can think of it as the ball counting up as it approaches the wall or paddle, and deciding to turn around when the count reaches the ball width.
51 |
52 | * What else would you like to add to this game?
53 |
54 | I'd wanted to make a slightly larger, rounder ball, but I ran out of bits. The efficient ball sizes are (2^N - 1) pixels across, so going up from 3x3, you can go all the way up to 7x7 for the same cost as 4x4, but that cost is unfortunately rather high...roughly 5 bits, and I've only got about 1 that's not entirely necessary right now. I can see an optimization that might make it possible, but I think I'm already hitting diminishing returns on my time in this project. The center dot on the ball is my little nod toward styling; it has no actual function, and uses that one extra-ish bit.
55 |
56 | * What other games could be implemented similarly?
57 |
58 | Well, [Breakout](https://en.wikipedia.org/wiki/Breakout_(video_game)) is an
59 | obvious next step, ideally with a local high score table, but I'd love to see
60 | if something like
61 | [Asteroids](https://en.wikipedia.org/wiki/Asteroids_(video_game)) could be
62 | done, with Life-style animations when you destroy an asteroid. What do you
63 | think, could you write [Pac Man](https://en.wikipedia.org/wiki/Pac-Man)?
64 |
65 | * Are you going to try to write any of those?
66 |
67 | Nope.
68 |
69 | * How long is a game?
70 |
71 | The first player to 10 points wins.
72 |
73 |
74 |
83 |
84 |
85 |
90 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/ballstate.js:
--------------------------------------------------------------------------------
1 | // We use a 5-bit encoding that covers the full 7x7 surrounding square,
2 | // for 32 real directions. Then we'll need 2 more bits for Bresenham state.
3 | // The first two bits of the five pick the quadrant, and then next 3 use a
4 | // lookup table to get their specifics. Could allocate one of those 3 to
5 | // whether x or y is primary, which may be convenient, but it's still not
6 | // symmetric after that. Here the highest bit indicates Y primary if set.
7 | // . . . . 7 5 .
8 | // . . . . 6 . 3
9 | // . . . . 4 2 1
10 | // . . . x 0 . .
11 | // . . . . . . .
12 | // . . . . . . .
13 | // . . . . . . .
14 |
15 | // Given the above encoding, to support the full range of motions, the
16 | // quadrant selection needs to be a rotation. However, since we'll never go
17 | // straight up or down in pong, we can do reflections instead, which is a
18 | // lot simpler for bounces, and we'll just have redundant encodings for straight
19 | // horizontal.
20 |
21 | (function () {
22 |
23 | const motionTable = [
24 | {
25 | dir: 'x',
26 | bInc: 0,
27 | bMax: 0,
28 | slope: 0,
29 | },
30 | {
31 | dir: 'x',
32 | bInc: 1,
33 | bMax: 3,
34 | slope: 1/3,
35 | },
36 | {
37 | dir: 'x',
38 | bInc: 1,
39 | bMax: 2,
40 | slope: 1/2,
41 | },
42 | {
43 | dir: 'x',
44 | bInc: 2,
45 | bMax: 3,
46 | slope: 2/3,
47 | },
48 | {
49 | dir: 'y', // whichever
50 | bInc: 1,
51 | bMax: 1,
52 | slope: 1,
53 | },
54 | {
55 | dir: 'y',
56 | bInc: 2,
57 | bMax: 3,
58 | slope: 3/2,
59 | },
60 | {
61 | dir: 'y',
62 | bInc: 1,
63 | bMax: 2,
64 | slope: 2,
65 | },
66 | {
67 | dir: 'y',
68 | bInc: 1,
69 | bMax: 3,
70 | slope: 3,
71 | },
72 | ]
73 |
74 | function processState(bs) {
75 | let dX = 0, dY = 0;
76 | let record = motionTable[bs.index];
77 |
78 | let nextState = bs.state + record.bInc;
79 | let overflow = false;
80 | if (record.bMax && nextState >= record.bMax) {
81 | overflow = true;
82 | nextState -= record.bMax;
83 | }
84 | if (record.dir == 'x') {
85 | dX = 1;
86 | if (overflow) {
87 | dY = 1;
88 | }
89 | } else {
90 | dY = 1;
91 | if (overflow) {
92 | dX = 1;
93 | }
94 | }
95 | if (!bs.right) {
96 | dX = -dX;
97 | }
98 | if (!bs.down) {
99 | dY = -dY;
100 | }
101 | bs.dX = dX;
102 | bs.dY = dY;
103 | bs.nextState = nextState;
104 | }
105 |
106 | // Note that these bit assignments are currently specific to various files.
107 |
108 | // Don't access color directly; it may be out of date.
109 | class BallState {
110 | constructor(ns, color) {
111 | // assert(ns.BALL_FLAG.isSet(color));
112 | this.ns = ns;
113 | this.color = color;
114 | this.right = ns.MOVE_R_NOT_L.get(color);
115 | this.down = ns.MOVE_D_NOT_U.get(color);
116 | this.state = ns.MOVE_STATE.get(color);
117 | this.index = ns.MOVE_INDEX.get(color);
118 | this.decimator = ns.DECIMATOR.get(color);
119 | this.depthX = ns.BUFFER_X_DEPTH_COUNTER.get(color);
120 | this.depthY = ns.BUFFER_Y_DEPTH_COUNTER.get(color);
121 | assert(this.index >= 0 && this.index < motionTable.length);
122 |
123 | processState(this);
124 | }
125 |
126 | isMotionCycle() {
127 | return this.decimator;
128 | }
129 |
130 | static create(ns, right, down, index, state, baseColor) {
131 | let color = baseColor;
132 | color = ns.MOVE_R_NOT_L.set(color, right);
133 | color = ns.MOVE_D_NOT_U.set(color, down);
134 | color = ns.MOVE_INDEX.set(color, index);
135 | color = ns.MOVE_STATE.set(color, state);
136 | return new BallState(ns, color);
137 | }
138 |
139 | reflect(axis) {
140 | if (axis === 'x') {
141 | this.right = this.right ^ 1;
142 | }
143 | else if (axis === 'y') {
144 | this.down = this.down ^ 1;
145 | } else {
146 | assert(false);
147 | }
148 | }
149 |
150 | reflectAngleInc(axis) {
151 | let d;
152 | if (axis === 'x') {
153 | this.right = this.right ^ 1;
154 | d = 'dX';
155 | }
156 | else if (axis === 'y') {
157 | this.down = this.down ^ 1;
158 | d = 'dY';
159 | } else {
160 | assert(false);
161 | }
162 | this.index = (this.index + 1) % 8;
163 | if (axis === 'y' && this.index === 0) {
164 | // Don't go horizontal from a Y bounce.
165 | this.index = 1;
166 | }
167 | // when changing index, reset state to stay valid
168 | this.state = 0;
169 | processState(this);
170 | // Here we want the *next* state to be the one moving off the wall, not the
171 | // current one, since that's the one we'll be returning.
172 | let nextBs = new BallState(this.ns, this.nextColor());
173 | while(Math.abs(nextBs[d]) < 0.5) {
174 | ++this.state;
175 | processState(this);
176 | nextBs = new BallState(this.ns, this.nextColor());
177 | }
178 | }
179 |
180 | bounce(axis, paddlePixel) {
181 | if (!this.index) {
182 | // It's level, so pretend the slope matches the paddle direction.
183 | this.down = paddlePixel > 3 ? 1 : 0;
184 | }
185 | let setIndex = false;
186 | if (paddlePixel !== undefined) {
187 | assert(axis === 'x');
188 | switch (paddlePixel) {
189 | case 0:
190 | case 7:
191 | if ((this.down !== 0) === (paddlePixel === 7)) {
192 | this.index = Math.min(this.index + 3, 7);
193 | } else {
194 | this.index = this.index - 3;
195 | if (this.index < 0) {
196 | this.index = -this.index;
197 | this.down = this.down ^ 1;
198 | }
199 | }
200 | setIndex = true;
201 | case 1:
202 | case 6:
203 | if ((this.down !== 0) === (paddlePixel === 6)) {
204 | this.index = Math.min(this.index + 2, 7);
205 | } else {
206 | this.index = this.index - 2;
207 | if (this.index < 0) {
208 | this.index = -this.index;
209 | this.down = this.down ^ 1;
210 | }
211 | }
212 | setIndex = true;
213 | break;
214 | case 2:
215 | case 5:
216 | if ((this.down !== 0) === (paddlePixel === 5)) {
217 | this.index = Math.min(this.index + 1, 7);
218 | } else {
219 | this.index = this.index - 1;
220 | if (this.index < 0) {
221 | this.index = -this.index;
222 | this.down = this.down ^ 1;
223 | }
224 | }
225 | setIndex = true;
226 | break;
227 | case 3: // natural reflection
228 | case 4:
229 | break;
230 | }
231 | }
232 | let d;
233 | if (axis === 'x') {
234 | this.right = this.right ^ 1;
235 | d = 'dX'
236 | }
237 | else if (axis === 'y') {
238 | this.down = this.down ^ 1;
239 | d = 'dY'
240 | processState(this);
241 | } else {
242 | assert(false);
243 | }
244 | if (setIndex || axis === 'x') {
245 | // Any time you change index, you may have to update state to a value
246 | // legal for the new index. Since we want the ball to come off the paddle
247 | // the cycle after it hits to avoid duplicate AI messages, we pick a state
248 | // that forces that.
249 | this.state = 0;
250 | processState(this);
251 | // Here we want the *next* state to be the one moving off the wall, not
252 | // the current one, since that's the one we'll be returning.
253 | let nextBs = new BallState(this.ns, this.nextColor());
254 | while(Math.abs(nextBs[d]) < 0.5) {
255 | ++this.state;
256 | processState(this);
257 | nextBs = new BallState(this.ns, this.nextColor());
258 | }
259 | }
260 | }
261 |
262 | getDepthX() {
263 | return this.depthX;
264 | }
265 |
266 | getDepthY() {
267 | return this.depthY;
268 | }
269 |
270 | setDepthX(d) {
271 | this.depthX = d;
272 | }
273 |
274 | setDepthY(d) {
275 | this.depthY = d;
276 | }
277 |
278 | incDepthX() {
279 | ++this.depthX;
280 | }
281 |
282 | incDepthY() {
283 | ++this.depthY;
284 | }
285 |
286 | decDepthX() {
287 | assert(this.depthX > 0);
288 | --this.depthX;
289 | }
290 |
291 | decDepthY() {
292 | assert(this.depthY > 0);
293 | --this.depthY;
294 | }
295 |
296 | getSlope() {
297 | return motionTable[this.index].slope;
298 | }
299 |
300 | getColor() {
301 | let color = this.color;
302 | color = this.ns.MOVE_R_NOT_L.set(color, this.right);
303 | color = this.ns.MOVE_D_NOT_U.set(color, this.down);
304 | color = this.ns.BUFFER_X_DEPTH_COUNTER.set(color, this.depthX);
305 | color = this.ns.BUFFER_Y_DEPTH_COUNTER.set(color, this.depthY);
306 | color = this.ns.MOVE_STATE.set(color, this.state);
307 | color = this.ns.MOVE_INDEX.set(color, this.index);
308 | return color;
309 | }
310 |
311 | // TODO: Incorporate DECIMATOR like PaddleState does.
312 | nextColor() {
313 | let color = this.getColor();
314 | if (this.isMotionCycle()) {
315 | color = this.ns.MOVE_STATE.set(color, this.nextState);
316 | }
317 | return color;
318 | }
319 | }
320 |
321 | window.BallState = BallState
322 | })();
323 |
--------------------------------------------------------------------------------
/bitmanager.js:
--------------------------------------------------------------------------------
1 | // You start with the global namespace. If you want to split into subspaces,
2 | // you have to pick which bits are the indicators. For example, you can declare
3 | // ID_0 and ID_1 as your two bits, and combine them together as ID_BITS. Then
4 | // you setSubspaceMask(ID_BITS). For each subspace you want to create, you then
5 | // declareSubspace(ID_0, 'BALL'), declareSubspace(ID_1, 'PADDLE'),
6 | // declareSubspace(ID_BITS, 'WALL'), declareSubspace(0, 'BACKGROUND'),
7 | // For the global namespace, just use new Namespace() with no arguments.
8 |
9 | // Backwards-compatibility shim; this only supports the bare minimum that's
10 | // already in use.
11 | function _or(a, b) {
12 | return (a | b) >>> 0;
13 | }
14 | function _and(a, b) {
15 | return (a & b) >>> 0;
16 | }
17 | function _orL(list) {
18 | assert(_.isArray(list));
19 | _.map(list, i => assert(_.isNumber(i)));
20 | return _.reduce(list, _or, 0);
21 | }
22 | function _andL(list) {
23 | assert(_.isArray(list));
24 | return _.reduce(list, _and, 0);
25 | }
26 |
27 | class BitManager {
28 | constructor(globalNamespace) {
29 | this.ns = globalNamespace;
30 | }
31 | // TODO: This could generate its own whichBits records by capturing them at
32 | // combine calls.
33 | static copyBits(nsTo, packedTo, nsFrom, packedFrom, whichBits) {
34 | assert(_.isArray(whichBits));
35 | for (let value of whichBits) {
36 | let bits = nsFrom[value].get(packedFrom);
37 | packedTo = nsTo[value].set(packedTo, bits);
38 | }
39 | return packedTo;
40 | }
41 | _findNamespace(name, packed) {
42 | let ns = this.ns;
43 | while (!(name in ns)) {
44 | let id = _and(packed, ns.subspaceMask);
45 | ns = ns.subspacesById[id];
46 | assert(ns);
47 | }
48 | return ns;
49 | }
50 | or(list) { // Non-static only for ease of call.
51 | return _orL(list);
52 | }
53 | and(list) { // Non-static only for ease of call.
54 | return _andL(list)
55 | }
56 | dumpStatus() {
57 | return this.ns.dumpStatus();
58 | }
59 | get(name, packed) {
60 | let ns = this._findNamespace(name, packed);
61 | return ns[name].get(packed);
62 | }
63 | getMask(name) {
64 | let ns = this._findNamespace(name, packed);
65 | return ns[name].getMask();
66 | }
67 | set(name, packed, value) {
68 | let ns = this._findNamespace(name, packed);
69 | return ns[name].set(packed, value);
70 | }
71 | setMask(name, packed, value) {
72 | let ns = this._findNamespace(name, packed);
73 | return ns[name].setMask(packed, value);
74 | }
75 | isSet(name, packed) {
76 | let ns = this._findNamespace(name, packed);
77 | return ns[name].isSet(packed);
78 | }
79 | // This is really hacky and should never be used, but is here for
80 | // backwards-compatibility.
81 | hasKey(name, nsName) {
82 | if (nsName) {
83 | return this.ns.subspacesByName[nsName] &&
84 | this.ns.subspacesByName[nsName].hasKey(name);
85 | }
86 | return this.ns.hasKey(name);
87 | }
88 | declare(name, count, offset, namespace) {
89 | assert(!namespace);
90 | this.ns.declare(name, count, offset);
91 | }
92 | combine(name, names, namespace) {
93 | assert(!namespace);
94 | this.ns.combine(name, names);
95 | }
96 | alias(newName, oldName, namespace) {
97 | assert(!namespace);
98 | this.ns.alias(newName, oldName);
99 | }
100 | }
101 |
102 | function getHasValueFunction(mask, value) {
103 | assert(_.isNumber(mask));
104 | assert(_.isNumber(value));
105 | return data => ((data & mask) >>> 0) === value;
106 | }
107 |
108 | class Namespace {
109 | constructor(name, parent, id) {
110 | assert(!name || (_.isString(name) && name.length && parent));
111 | this.name = name || 'GLOBAL';
112 | this.parent = parent;
113 | this.id = id;
114 | this.subspacesByName = {};
115 | this.subspacesById = {};
116 | this.values = {};
117 | this.bitsUsed = parent ? parent.bitsUsed : 0;
118 | }
119 | dumpStatus() {
120 | console.log('bits used by', this.name, this.bitsUsed.toString(16));
121 | for (let name in this.subspacesByName) {
122 | this.subspacesByName[name].dumpStatus();
123 | }
124 | }
125 |
126 | getDescription(packed, prefix) {
127 | if (!prefix) {
128 | prefix = ''
129 | }
130 | var s;
131 | s = prefix + 'namespace: ' + this.name;
132 | prefix += ' ';
133 | let names = _.sortBy(Object.keys(this.values));
134 | for (var name of names) {
135 | var value = this.values[name];
136 | let v = value.get(packed);
137 | s += '\n' + prefix + name + ': 0x' + v.toString(16);
138 | }
139 | if (this.subspaceMask) {
140 | let id = _and(packed, this.subspaceMask);
141 | assert(id in this.subspacesById)
142 | s += '\n' + this.subspacesById[id].getDescription(packed, prefix);
143 | }
144 | return s;
145 | }
146 |
147 | describe(packed, prefix) {
148 | console.log(this.getDescription(packed, prefix));
149 | }
150 |
151 | setSubspaceMask(maskName) {
152 | assert(this.subspaceMask === undefined);
153 | let mask = 0;
154 | if (this.parent) {
155 | mask = this.parent.subspaceMask;
156 | }
157 | this.subspaceMask = _or(mask, this.values[maskName].getMask());
158 | }
159 | declareSubspace(name, idMaskNameOrZero) {
160 | assert(this.subspaceMask);
161 | assert(!(name in this.subspacesByName));
162 | let id = 0;
163 | if (_.isString(idMaskNameOrZero)) {
164 | id = this.values[idMaskNameOrZero].getMask();
165 | } else {
166 | assert(idMaskNameOrZero === 0);
167 | }
168 | if (this.parent) {
169 | id = _or(id, this.id);
170 | }
171 | assert(!(id & ~this.subspaceMask));
172 | assert(!(id in this.subspacesById));
173 | let subspace = new Namespace(name, this, id);
174 | this.subspacesByName[name] = subspace;
175 | this.subspacesById[id] = subspace;
176 | return subspace;
177 | }
178 | declare(name, count, offset) {
179 | // Can't use up any more bits in the mask after declaring subspaces. Do all
180 | // your bits first.
181 | assert(!Object.keys(this.subspacesById).length);
182 | let bits = ((1 << count) - 1) >>> 0;
183 | let mask = (bits << offset) >>> 0
184 | this._ensureNonConflicting(name, mask);
185 |
186 | assert(!(name in this.values));
187 | assert(!(name in this));
188 | this.bitsUsed = _or(this.bitsUsed, mask);
189 | let record = {
190 | offset: offset,
191 | bits: bits,
192 | mask: mask,
193 | count: count
194 | }
195 | let newValue = new Value(this, name, record);
196 | this.values[name] = newValue;
197 | // This is the speedy accessor; it's a bit hacky, but it means you can look
198 | // for namespace.VALUE_NAME and have it work, instead of
199 | // namespace.records.VALUE_NAME. Is it worth it?
200 | this[name] = newValue;
201 | return newValue;
202 | }
203 | alloc(name, count) {
204 | // Can't use up any more bits in the mask after declaring subspaces. Do all
205 | // your bits first.
206 | assert(!Object.keys(this.subspacesById).length);
207 | assert(!(name in this.values));
208 | assert(!(name in this));
209 | let bits = ((1 << count) - 1) >>> 0;
210 | let offset = 0;
211 | let mask = bits;
212 | while (mask & this.bitsUsed) {
213 | ++offset;
214 | assert(offset + count < 32);
215 | mask <<= 1;
216 | }
217 |
218 | this.bitsUsed = _or(this.bitsUsed, mask);
219 | let record = {
220 | offset: offset,
221 | bits: bits,
222 | mask: mask,
223 | count: count
224 | }
225 | let newValue = new Value(this, name, record);
226 | this.values[name] = newValue;
227 | // This is the speedy accessor; it's a bit hacky, but it means you can look
228 | // for namespace.VALUE_NAME and have it work, instead of
229 | // namespace.records.VALUE_NAME. Is it worth it?
230 | this[name] = newValue;
231 | return newValue;
232 | }
233 | // NOTE: Only works within a single namespace.
234 | alias(newName, oldName) {
235 | assert(oldName in this.values);
236 | assert(!(newName in this.values));
237 | let record = this.values[oldName].record;
238 | let newValue = new Value(this, newName, record);
239 | this.values[newName] = newValue;
240 | this[newName] = newValue;
241 | return newValue;
242 | }
243 |
244 | _findRecord(name) {
245 | if (name in this.values) {
246 | return this.values[name]
247 | }
248 | assert(this.parent);
249 | return this.parent._findRecord(name)
250 | }
251 |
252 | combine(newName, oldNames) {
253 | assert(!(newName in this.values));
254 | assert(_.isArray(oldNames));
255 | let mask = 0;
256 | let offset = 32;
257 | for (var name of oldNames) {
258 | let oldValue = this._findRecord(name);
259 | mask = _or(mask, oldValue.getMask());
260 | offset = Math.min(offset, oldValue.getOffset());
261 | }
262 | let bits = mask >>> offset;
263 | let record = {
264 | offset: offset,
265 | bits: bits,
266 | mask: mask
267 | };
268 | let newValue = new Value(this, newName, record);
269 | this.values[newName] = newValue;
270 | this[newName] = newValue;
271 | return newValue;
272 | }
273 |
274 | _ensureNonConflicting(name, mask) {
275 | if (this.bitsUsed & mask) {
276 | for (let r in this.values) {
277 | let value = this.values[r];
278 | if (value.getMask() & mask) {
279 | throw new Error(
280 | `Declaration of "${name}" conflicts with "${r}".`)
281 | }
282 | }
283 | // Found a conflict but it's not here, so it must be in the parent.
284 | this.parent._ensureNonConflicting(name, mask);
285 | assert(false);
286 | }
287 | }
288 |
289 | // This is really hacky and should never be used, but is here for
290 | // backwards-compatibility.
291 | hasKey(name) {
292 | if (name in this.values) {
293 | return true;
294 | }
295 | for (let i in this.subspacesById) {
296 | if (this.subspacesById[i].hasKey(name)) {
297 | return true;
298 | }
299 | }
300 | return false;
301 | }
302 | }
303 |
304 | class Value {
305 | constructor (namespace, name, record) {
306 | this.name = name;
307 | this.namespace = namespace; // for debugging only, currently
308 | this.record = record;
309 | this.namespaceId = 0;
310 | this.namespaceMask = 0;
311 | if (namespace.parent) {
312 | this.namespaceId = namespace.id;
313 | this.namespaceMask = namespace.parent.subspaceMask;
314 | }
315 | }
316 |
317 | isSet(packed) {
318 | assert(_.isNumber(packed));
319 | assert(_and(packed, this.namespaceMask) === this.namespaceId);
320 | return _and(packed, this.record.mask) === this.record.mask;
321 | }
322 |
323 | get(packed) {
324 | assert(_.isNumber(packed));
325 | assert(_and(packed, this.namespaceMask) === this.namespaceId);
326 | return (packed & this.record.mask) >>> this.record.offset;
327 | }
328 |
329 | getMask() {
330 | return this.record.mask;
331 | }
332 |
333 | getOffset() {
334 | return this.record.offset;
335 | }
336 |
337 | set(packed, value) {
338 | assert(_.isNumber(packed));
339 | assert(_and(packed, this.namespaceMask) === this.namespaceId);
340 | if (!_.isNumber(value)) {
341 | assert(_.isBoolean(value));
342 | assert(this.record.count === 1);
343 | value = value ? 1 : 0;
344 | }
345 | assert(!(value & ~this.record.bits));
346 | return ((packed & ~this.record.mask) | (value << this.record.offset)) >>> 0;
347 | }
348 |
349 | setMask(packed, value, namespace) {
350 | assert(_.isNumber(packed));
351 | assert(_.isBoolean(value));
352 | assert(_and(packed, this.namespaceMask) === this.namespaceId);
353 | if (value) {
354 | return _or(packed, this.record.mask);
355 | } else {
356 | return _and(packed, ~this.record.mask);
357 | }
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/canvaswrapper.js:
--------------------------------------------------------------------------------
1 | // We can't trust canvas to do bit-exact alpha values, since it has to translate
2 | // int -> float -> int.
3 | class CanvasWrapper {
4 | constructor(data) {
5 | this.data = data;
6 | this.view = new Uint32Array(this.data.data.buffer);
7 | }
8 |
9 | getAddr32(i, j) {
10 | return i + this.data.width * j
11 | }
12 |
13 | fillRectWith(color, op, x, y, w, h) {
14 | assert(_.isNumber(color));
15 | assert(_.isString(op));
16 | if (x < 0) {
17 | x = 0;
18 | }
19 | if (x >= this.data.width) {
20 | x = this.data.width - 1;
21 | }
22 | if (y < 0) {
23 | y = 0;
24 | }
25 | if (y >= this.data.height) {
26 | y = this.data.height - 1;
27 | }
28 | if (x + w >= this.data.width) {
29 | w = this.data.width - x;
30 | }
31 | if (y + h >= this.data.height) {
32 | h = this.data.height - y;
33 | }
34 | for (let i = 0; i < w; ++i) {
35 | for (let j = 0; j < h; ++j) {
36 | if (op === 'or') {
37 | this.view[this.getAddr32(i + x, j + y)] |= color;
38 | } else {
39 | assert(op === 'set');
40 | this.view[this.getAddr32(i + x, j + y)] = color;
41 | }
42 | }
43 | }
44 | }
45 |
46 | orRect(color, x, y, w, h) {
47 | return this.fillRectWith(color, 'or', x, y, w, h);
48 | }
49 |
50 | fillRect(color, x, y, w, h) {
51 | return this.fillRectWith(color, 'set', x, y, w, h);
52 | }
53 |
54 | strokeRect(color, x, y, w, h) {
55 | if (x < 0) {
56 | x = 0;
57 | }
58 | if (x >= this.data.width) {
59 | x = this.data.width - 1;
60 | }
61 | if (y < 0) {
62 | y = 0;
63 | }
64 | if (y >= this.data.height) {
65 | y = this.data.height - 1;
66 | }
67 | if (x + w >= this.data.width) {
68 | w = this.data.width - x;
69 | }
70 | if (y + h >= this.data.height) {
71 | h = this.data.height - y;
72 | }
73 | --w; --h;
74 | for (let i = 0; i < w; ++i) {
75 | this.view[this.getAddr32(i + x, y)] = color;
76 | this.view[this.getAddr32(i + x, y + h)] = color;
77 | }
78 | for (let j = 0; j <= h; ++j) {
79 | this.view[this.getAddr32(x, j + y)] = color;
80 | this.view[this.getAddr32(x + w, j + y)] = color;
81 | }
82 | }
83 |
84 | fillBitmap(x, y, message, key) {
85 | assert(x >= 0);
86 | assert(y >= 0);
87 | for (let j = 0; j < message.length; ++j) {
88 | assert(y + j < this.data.height);
89 | let line = message[j];
90 | for (let i = 0; i < line.length; ++i) {
91 | assert(x + i < this.data.width);
92 | assert(message[j][i] in key)
93 | this.view[this.getAddr32(i + x, j + y)] = key[message[j][i]];
94 | }
95 | }
96 | }
97 |
98 | }
99 |
100 |
101 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |