├── .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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 22 | 28 |
29 |
30 | 37 | 43 |
44 |
45 |
46 | Keyboard controls: 47 |
48 |
49 |
50 | Left player: w/s 51 |
52 |
53 | Right player: arrows 54 |
55 |
56 | Pause/continue: space 57 |
58 |
59 |
60 |
61 | 67 | 73 | Source 74 |
75 |
76 | 99 |
100 |
101 |
102 | 103 |
104 |
105 | 106 |
107 | 108 |
109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 124 | 128 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /keyboard.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.addEventListener('keydown', onKeyDown); 3 | window.addEventListener('keyup', onKeyUp); 4 | 5 | let keyTable = { }; 6 | function onKeyDown(event) { 7 | let key = event.key.toLowerCase(); 8 | if (!keyTable[key]) { 9 | keyTable[key] = true; 10 | } 11 | if (event.key === ' ') { 12 | document.getElementById('toggle_run').click(); 13 | event.preventDefault(); 14 | } 15 | if (_.indexOf(['w', 's', 'arrowleft', 'arrowright', 'arrowup', 'arrowdown', 16 | ' '], 17 | key) !== -1) { 18 | event.preventDefault(); 19 | } 20 | } 21 | 22 | function onKeyUp(event) { 23 | let key = event.key.toLowerCase(); 24 | keyTable[key] = false; 25 | if (_.indexOf(['w', 's', 'arrowup', 'arrowdown', ' '], key) !== -1) { 26 | event.preventDefault(); 27 | } 28 | } 29 | 30 | function keyIsPressed(lowerCaseChar) { 31 | return keyTable[lowerCaseChar]; 32 | } 33 | window.keyIsPressed = keyIsPressed; 34 | 35 | })() 36 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: row; 4 | width: 100%; 5 | height: 100%; 6 | max-width: 100vw; 7 | max-height: 100vh; 8 | margin: 0; 9 | } 10 | 11 | button { 12 | min-width: 100px; 13 | min-height: 40px; 14 | } 15 | 16 | input[type="checkbox"] { 17 | min-width: 30px; 18 | min-height: 30px; 19 | vertical-align: middle; 20 | } 21 | 22 | #fps_parent { 23 | width: 100px; 24 | min-width: 100px; 25 | max-width: 100px; 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: center; 29 | align-items: middle; 30 | } 31 | 32 | select { 33 | min-width: 100px; 34 | min-height: 40px; 35 | } 36 | 37 | .basic-info { 38 | display: flex; 39 | flex-direction: row; 40 | flex-wrap: wrap; 41 | align-items: stretch; 42 | justify-content: center; 43 | border: 1px solid black; 44 | } 45 | 46 | .player-select { 47 | display: flex; 48 | flex-direction: column; 49 | border: 1px solid black; 50 | flex: 1 1 auto; 51 | justify-content: center; 52 | } 53 | 54 | #controls { 55 | display: flex; 56 | flex-direction: row; 57 | justify-content: center; 58 | flex: 0 0 auto; 59 | height: auto; 60 | flex-wrap: wrap; 61 | } 62 | #controls * { 63 | margin: 5px; 64 | } 65 | 66 | #keyboard-info { 67 | display: flex; 68 | flex-direction: row; 69 | align-items: center; 70 | border: 1px solid black; 71 | } 72 | 73 | #keyboard-controls-list { 74 | flex: 2 1 auto; 75 | display: flex; 76 | flex-direction: column; 77 | align-items: flex-start; 78 | div { 79 | display: flex; 80 | flex-wrap: nowrap; 81 | flex-direction: row; 82 | } 83 | } 84 | 85 | #keyboard-controls-label { 86 | flex: 1 1 auto; 87 | } 88 | 89 | #checkboxes { 90 | flex: 1 1 auto; 91 | display: flex; 92 | flex-direction: row; 93 | flex-wrap: wrap; 94 | justify-content: center; 95 | align-items: center; 96 | } 97 | 98 | #debugging-controls { 99 | display: flex; 100 | flex-direction: row; 101 | align-items: center; 102 | flex-wrap: wrap; 103 | justify-content: center; 104 | } 105 | 106 | #container { 107 | display: flex; 108 | flex-direction: column; 109 | flex: 1 1 auto; 110 | align-items: center; 111 | width: 100%; 112 | max-width: 100%; 113 | } 114 | 115 | #inner-container { 116 | display: flex; 117 | flex-direction: row; 118 | flex: 1 1 auto; 119 | flex-wrap: wrap; 120 | width: 100%; 121 | height: 100%; 122 | align-items: stretch; 123 | } 124 | 125 | .canvas-container { 126 | flex: 1 1 auto; 127 | position: relative; 128 | } 129 | 130 | canvas { 131 | image-rendering: crisp-edges; 132 | image-rendering: pixelated; 133 | background-color: #000; 134 | position: absolute; 135 | top: 0; 136 | bottom: 0; 137 | left: 0; 138 | right: 0; 139 | margin: auto; 140 | } 141 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (function () { 4 | 5 | let canvas, canvas2; 6 | let context, context2; 7 | // inputBuffer starts with canvas image data. We write to outputBuffer, 8 | // then copy that either to the canvas or to canvas2 for display, and into 9 | // inputBuffer for the next iteration. 10 | let inputBuffer; 11 | let inputView; 12 | let outputBuffer; 13 | let outputBuffer2; 14 | let outputView; 15 | let outputView2; 16 | let activeWidth; 17 | let activeHeight; 18 | const borderSize = 1; // Leave a 1-pixel sentinel border. 19 | const originX = borderSize; 20 | const originY = borderSize; 21 | 22 | function init() { 23 | canvas = document.getElementById('canvas'); 24 | canvas2 = document.getElementById('canvas2'); 25 | canvas2.width = canvas.width; 26 | canvas2.height = canvas.height; 27 | context = canvas.getContext('2d'); 28 | context2 = canvas2.getContext('2d'); 29 | context.clearRect(0, 0, canvas.width, canvas.height); 30 | context2.clearRect(0, 0, canvas2.width, canvas2.height); 31 | showTestToggled(); 32 | showDebugToggled(); 33 | showDebugControlsToggled(); 34 | getColorInfo(); 35 | getPlayerInfo(); 36 | canvas.addEventListener('click', onCanvasClicked); 37 | canvas2.addEventListener('click', onCanvasClicked); 38 | window.addEventListener('resize', resizeCanvasCSS); 39 | initAnimation(); 40 | toggleRun(); 41 | } 42 | window.init = init; 43 | 44 | function setDimensions(w, h) { 45 | canvas.width = w; 46 | canvas.height = h; 47 | canvas2.width = w; 48 | canvas2.height = h; 49 | activeWidth = canvas.width - 2 * borderSize; 50 | activeHeight = canvas.height - 2 * borderSize; 51 | resizeCanvasCSS(); 52 | } 53 | 54 | function resizeCanvasCSS() { 55 | function helper(container, canvas) { 56 | let rect = container.getBoundingClientRect(); 57 | let boundingWidth = rect.width; 58 | let boundingHeight = rect.height; 59 | if (!(boundingWidth && boundingHeight && canvas.width && canvas.height)) { 60 | return; // not yet initialized 61 | } 62 | let ratio = canvas.height / canvas.width; 63 | if (boundingWidth * ratio > boundingHeight) { 64 | // height is the limiting dimension 65 | canvas.style.height = boundingHeight; 66 | canvas.style.width = boundingHeight / ratio; 67 | } else { 68 | canvas.style.height = boundingWidth * ratio; 69 | canvas.style.width = boundingWidth; 70 | } 71 | } 72 | helper(document.getElementById('canvas-container'), canvas); 73 | helper(document.getElementById('canvas2-container'), canvas2); 74 | } 75 | 76 | function initBuffers() { 77 | inputBuffer = context.getImageData(0, 0, canvas.width, canvas.height); 78 | outputBuffer = context.createImageData(inputBuffer) 79 | outputBuffer2 = context.createImageData(inputBuffer) 80 | inputView = new Uint32Array(inputBuffer.data.buffer); 81 | outputView = new Uint32Array(outputBuffer.data.buffer); 82 | outputView2 = new Uint32Array(outputBuffer2.data.buffer); 83 | } 84 | 85 | var curFunc; 86 | 87 | let animation; 88 | function initAnimation() { 89 | // Add 2 for the sentinel borders, which the animation doesn't think 90 | // about. 91 | setDimensions(animation.width + 2, animation.height + 2); 92 | initBuffers(); 93 | let c = new CanvasWrapper(outputBuffer); 94 | c.fillRect(0, 0, 0, canvas.width, canvas.height); 95 | animation.init(c, originX, originY, activeWidth, activeHeight, 96 | OBVIOUS_COLORS); 97 | context.clearRect(0, 0, canvas.width, canvas.height); 98 | context.putImageData(outputBuffer, 0, 0); 99 | inputView.set(outputView); 100 | curFunc = animation.f; 101 | } 102 | 103 | function registerAnimation(name, width, height, init, f) { 104 | animation = { 105 | init: init, 106 | f: f, 107 | name: name, 108 | width: width, 109 | height: height 110 | }; 111 | } 112 | window.registerAnimation = registerAnimation; 113 | 114 | function getAddr32(i, j) { 115 | return i + canvas.width * j 116 | } 117 | 118 | function dumpImageData(view, x, y, w, h) { 119 | for (let j = y; j < y + h; ++j) { 120 | let addr = getAddr32(x, j); 121 | var t = _.map(view.slice(addr, addr + w), i => i.toString(16)).join(', '); 122 | console.log(t); 123 | } 124 | } 125 | window.dumpImageData = dumpImageData; 126 | 127 | function dumpBoard(x, y, w, h) { 128 | console.log('board state:'); 129 | x = x || 0; 130 | y = y || 0; 131 | w = w || canvas.width; 132 | h = h || canvas.height; 133 | dumpImageData(inputView, x, y, w, h); 134 | } 135 | window.dumpBoard = dumpBoard; 136 | 137 | // TODO: Look into array.subarray instead of the topData, midData, botData 138 | // copies. Could be cleaner and faster; faster still if we passed them into 139 | // f instead of the flattened array, assuming that was useful to f. 140 | function runConv3x3Step(f, inputView, outputView) { 141 | let inputRow = inputView.subarray(0, canvas.width); 142 | let outputRow = outputView.subarray(0, canvas.width); 143 | outputRow.set(inputRow); 144 | let addr = getAddr32(0, canvas.height - 1); 145 | inputRow = inputView.subarray(addr, addr + canvas.width); 146 | outputRow = outputView.subarray(addr, addr + canvas.width); 147 | outputRow.set(inputRow); 148 | for (let j = 0; j < canvas.height; ++j) { 149 | addr = getAddr32(0, j); 150 | outputView[addr] = inputView[addr]; 151 | outputView[addr + canvas.width - 1] = inputView[addr + canvas.width - 1]; 152 | } 153 | 154 | for (let j = originY; j < canvas.height - borderSize; ++j) { 155 | let i = originX; 156 | let topAddr = getAddr32(i - 1, j - 1); 157 | let topData = [0]; // placeholder 158 | topData.push(inputView[topAddr++]) 159 | topData.push(inputView[topAddr++]) 160 | 161 | let midAddr = getAddr32(i - 1, j); 162 | let midData = [0]; // placeholder 163 | midData.push(inputView[midAddr++]) 164 | midData.push(inputView[midAddr++]) 165 | 166 | let botAddr = getAddr32(i - 1, j + 1); 167 | let botData = [0]; // placeholder 168 | botData.push(inputView[botAddr++]) 169 | botData.push(inputView[botAddr++]) 170 | 171 | let outputAddr = getAddr32(i, j); 172 | for (; i < canvas.width - borderSize; ++i, ++outputAddr) { 173 | topData.shift(); 174 | topData.push(inputView[topAddr++]) 175 | midData.shift(); 176 | midData.push(inputView[midAddr++]) 177 | botData.shift(); 178 | botData.push(inputView[botAddr++]) 179 | 180 | let value = f(_.flatten([topData, midData, botData]), i, j) 181 | outputView[outputAddr] = value; 182 | } 183 | } 184 | } 185 | 186 | function test() { 187 | runConv3x3Step(curFunc, inputView, outputView2); 188 | context2.putImageData(outputBuffer2, 0, 0); 189 | } 190 | window.test = test; 191 | 192 | function step() { 193 | runConv3x3Step(curFunc, inputView, outputView); 194 | context.putImageData(outputBuffer, 0, 0, 195 | originX, originY, activeWidth, activeHeight); 196 | inputView.set(outputView); 197 | } 198 | window.step = step; 199 | 200 | function showTestToggled() { 201 | if (document.getElementById('toggle_test').checked) { 202 | document.getElementById('canvas2').parentElement.style.display = 'inline'; 203 | } else { 204 | document.getElementById('canvas2').parentElement.style.display = 'none'; 205 | } 206 | resizeCanvasCSS(); 207 | } 208 | window.showTestToggled = showTestToggled; 209 | 210 | function showDebugToggled() { 211 | if (document.getElementById('toggle_debug').checked) { 212 | document.getElementById('debug').style.display = 'inline'; 213 | } else { 214 | document.getElementById('debug').style.display = 'none'; 215 | } 216 | resizeCanvasCSS(); 217 | } 218 | window.showDebugToggled = showDebugToggled; 219 | 220 | function showDebugControlsToggled() { 221 | if (document.getElementById('toggle_debug_controls').checked) { 222 | document.getElementById('debugging-controls').style.display = 'flex'; 223 | } else { 224 | document.getElementById('debugging-controls').style.display = 'none'; 225 | } 226 | resizeCanvasCSS(); 227 | } 228 | window.showDebugControlsToggled = showDebugControlsToggled; 229 | 230 | let OBVIOUS_COLORS; 231 | function getColorInfo() { 232 | OBVIOUS_COLORS = document.getElementById('toggle_obvious').checked; 233 | } 234 | function showObviousToggled() { 235 | getColorInfo(); 236 | initAnimation(); 237 | } 238 | window.showObviousToggled = showObviousToggled; 239 | 240 | window.leftPlayerHuman = false; 241 | window.rightPlayerHuman = false; 242 | function getPlayerInfo() { 243 | leftPlayerHuman = 244 | document.getElementById('select_left_player_human').checked; 245 | rightPlayerHuman = 246 | document.getElementById('select_right_player_human').checked; 247 | } 248 | function playerToggled() { 249 | getPlayerInfo(); 250 | initAnimation(); 251 | } 252 | window.playerToggled = playerToggled; 253 | 254 | let frameReady = false; 255 | let frameInProgress = false; 256 | function asyncStep() { 257 | runConv3x3Step(curFunc, inputView, outputView); 258 | frameReady = true; 259 | frameInProgress = false; 260 | } 261 | 262 | function asyncAnimationFrame(timestamp) { 263 | if (running) { 264 | if (frameReady) { 265 | frameReady = false; 266 | context.putImageData(outputBuffer, 0, 0, 267 | originX, originY, activeWidth, activeHeight); 268 | inputView.set(outputView); 269 | window.setTimeout(asyncStep, 0); 270 | updateFPS(timestamp); 271 | } else if (!frameInProgress) { 272 | window.setTimeout(asyncStep, 0); 273 | } 274 | requestAnimationFrame(asyncAnimationFrame); 275 | } else { 276 | resetFPS(); 277 | } 278 | } 279 | 280 | function onCanvasClicked(e) { 281 | let xScale = canvas.clientWidth / canvas.width; 282 | let yScale = canvas.clientHeight / canvas.height; 283 | let x = Math.floor(e.offsetX / xScale); 284 | let y = Math.floor(e.offsetY / yScale); 285 | let addr = getAddr32(x, y); 286 | let view; 287 | if (e.currentTarget.id === 'canvas') { 288 | view = inputView; // Not outputView, which may differ. 289 | } else { 290 | view = outputView2; 291 | assert(e.currentTarget.id === 'canvas2'); 292 | } 293 | let value = view[addr] 294 | // Assumes the animation attaches ns to canvas on selection. 295 | let s = `(${x},${y}):${value.toString(16)}\n` + 296 | canvas.ns.getDescription(view[addr]) 297 | document.getElementById('debug').value = s; 298 | } 299 | 300 | let running = false; 301 | function animationFrame(timestamp) { 302 | if (running) { 303 | step(); 304 | updateFPS(timestamp); 305 | requestAnimationFrame(animationFrame); 306 | } else { 307 | resetFPS(); 308 | } 309 | } 310 | 311 | /* On frame callback: 312 | If you have a frame ready to show, copy it in, tell updateFPS about it, and 313 | kick off the next compute in the background. 314 | Update FPS. 315 | Request the next frame. 316 | 317 | On frame completion: mark the frame ready to show. 318 | */ 319 | function toggleRun() { 320 | running = !running; 321 | if (running) { 322 | requestAnimationFrame(asyncAnimationFrame); 323 | } 324 | } 325 | window.toggleRun = toggleRun; 326 | 327 | var fpsFrames = 0; 328 | var fpsStartTime = -1; 329 | 330 | function resetFPS() { 331 | fpsFrames = 0; 332 | fpsStartTime = -1; 333 | document.getElementById('fps').innerText = "N/A"; 334 | } 335 | 336 | function updateFPS(timestamp) { 337 | if (fpsStartTime < 0) { 338 | fpsStartTime = timestamp; 339 | } else { 340 | ++fpsFrames 341 | var timeDiff = timestamp - fpsStartTime 342 | // If it's been over a second and we've done something, update. 343 | if (timeDiff >= 1000 && fpsFrames > 0) { 344 | let fps = fpsFrames * 1000 / timeDiff 345 | document.getElementById('fps').innerText = fps.toFixed(3) 346 | fpsFrames = 0; 347 | fpsStartTime = timestamp 348 | } 349 | } 350 | } 351 | })() 352 | 353 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | TODO: 2 | Nicer scoreboard font using 1-hot encoding? 3 | 4 | Consider that crazy optimization for more bits, to do a larger ball with better 5 | shading? How many bits do we need, and how many do we have? 6 | 7 | Needed: 8 | 2:ball:BUFFER_X/Y_DEPTH_COUNTER, 9 | 2:bg+ball:MIN/MAX for BUFFER_X/Y_FLAG, 10 | 1:bg+ball:PADDLE_MOVE_DELAY_COUNTER. 11 | 1+:ball:shading, 12 | 0+:ball+bg+paddle bigger paddle means ideally a larger paddlePixel range, 13 | 1:bg:isCenterRespawn/respawnPhase2 will probably need at least one more 14 | bit. 15 | Minimum of 5 for ball, plus shading. 16 | 17 | -------------------------------------------------------------------------------- 18 | Cleanup: 19 | 20 | Consider making DECIMATOR a global bit. Almost everybody uses it, and it would 21 | be nice just to be able to grab it and negate it everywhere. It's not much more 22 | work for the empty background; might even be a wash, with less checking for if 23 | you need it or not. The only other minus would be having it in the scoreboard, 24 | which has no need for it, but that seems fairly trivial, especially if it's not 25 | a very visible bit. 26 | 27 | -------------------------------------------------------------------------------- 28 | 29 | Small ways to shave bits: 30 | 31 | 1: Use PADDLE_BUFFER_FLAG as a namespace bit for balls. All the paddle buffer 32 | bits go in that namespace, but then we can put BUFFER_Y_FLAG in its complement, 33 | since that information can be derived from the paddle's position and 34 | paddlePixel. 35 | 36 | 2: FULL_ALPHA is currently 3 bits in the background; we certainly wouldn't miss 37 | the bottom one, and could function without the bottom two if necessary. We've 38 | already trimmed down to 1-2 in the foreground; it's dimmer, but not too bad, 39 | given that it gets the high bit just for being foreground. 40 | 41 | 3: Can we share bits for MESSAGE_PADDLE_POSITION and PADDLE_DEST in the 42 | background? We can't get a message while we're moving, so PADDLE_DEST should be 43 | 0 when MESSAGE_PADDLE_POSITION is set, and then PADDLE_MOVE_DELAY_COUNTER will 44 | keep us from acting on the MESSAGE_PADDLE_POSITION until we're ready. Seems 45 | likely. That only saves us bits in nsBackground, though, not nsBall, which is 46 | generally shorter on bits. 47 | -------------------------------------------------------------------------------- 48 | The checkerboard ball state will probably work! We can save a ton of bits that 49 | way. Make a ball out of alternating pixels, where some have all the ball motion 50 | info and the others have all the collected paddle/buffer info. Both need the 51 | isPaddleBuffer and isBall bits. When you need one or the other namespace of 52 | info, there's always a pixel within reach that has it. The only thing to watch 53 | out for is that the leading corner when moving diagonally must have the ball 54 | info, as it may be the only ball pixel that the target pixel can see. That's 55 | probably pretty easy to arrange, though, for most odd ball sizes, and anybody 56 | outside the ball should be able to see the paddle buffer state directly in most 57 | cases, and see at least one paddle buffer pixel inside the ball otherwise. 58 | 59 | 7: 60 | x.x 61 | x.x.x 62 | x.x.x.x 63 | .x.x.x. 64 | x.x.x.x 65 | x.x.x 66 | x.x 67 | 68 | 5: 69 | x 70 | x.x 71 | x.x.x 72 | x.x 73 | x 74 | 75 | 4: does that work? Probably. And the bitflag would make for nice shading here, 76 | too. Start with a 3x3 ball for the switchover, then grow to this once it's 77 | working. 78 | 79 | xx 80 | x..x 81 | x..x 82 | xx 83 | 84 | 3: x 85 | x.x 86 | x 87 | -------------------------------------------------------------------------------- /paddlestate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let nsPaddle, nsBall, nsBackground; 3 | let isPaddle, isBallInPaddleBuffer, isPaddleBuffer; 4 | 5 | // Don't access color directly; it may be out of date. 6 | // This originally just dealt with paddles. You can now create a PaddleState 7 | // from a PaddleBuffer, with or without a ball in it, so beware the underlying 8 | // ball bits and type when dealing with motion. 9 | // Note that in setDY we assume a position range of 0-56, since that's what 10 | // our current 3-bit-shifted-3 dest can encode. 11 | class PaddleState { 12 | static init(_nsPaddle_, _nsBall_, _nsBackground_, 13 | _isPaddle_, _isBallInPaddleBuffer_, _isPaddleBuffer_) { 14 | nsPaddle = _nsPaddle_; 15 | nsBall = _nsBall_; 16 | nsBackground = _nsBackground_; 17 | isPaddle = _isPaddle_; 18 | isBallInPaddleBuffer = _isBallInPaddleBuffer_; 19 | isPaddleBuffer = _isPaddleBuffer_; 20 | } 21 | constructor(color) { 22 | assert(nsPaddle); 23 | assert(isPaddle(color) || isBallInPaddleBuffer(color) || 24 | isPaddleBuffer(color)); 25 | this.color = color; 26 | this.delay = 0; 27 | if (isPaddle(color)) { 28 | this.ns = nsPaddle; 29 | } else if (isBallInPaddleBuffer(color)) { 30 | this.ns = nsBall; 31 | } else { 32 | assert(isPaddleBuffer(color)); 33 | this.ns = nsBackground; 34 | this.delay = nsBackground.PADDLE_MOVE_DELAY_COUNTER.get(color); 35 | } 36 | this.position = this.ns.PADDLE_POSITION.get(color); 37 | this.dest = this.ns.PADDLE_DEST.get(color); 38 | this.decimator = this.ns.DECIMATOR.get(color); 39 | // This is the single raw bit, not the decoded, useful value. 40 | this.paddlePixelBit = this.ns.PADDLE_PIXEL.get(color); 41 | } 42 | 43 | isMotionCycle() { 44 | return !this.decimator; 45 | } 46 | 47 | getDY(isLeft) { 48 | assert(isLeft !== undefined); 49 | let useUserInput = isLeft ? leftPlayerHuman : rightPlayerHuman; 50 | if (useUserInput) { 51 | if (isLeft) { 52 | if (keyIsPressed('w') && this.position > 0) { 53 | return -1; 54 | } else if (keyIsPressed('s') && this.position < 56) { 55 | return 1; 56 | } else { 57 | return 0; 58 | } 59 | } else { 60 | if (keyIsPressed('arrowup') && this.position > 0) { 61 | return -1; 62 | } else if (keyIsPressed('arrowdown') && this.position < 56) { 63 | return 1; 64 | } else { 65 | return 0; 66 | } 67 | } 68 | } 69 | if (this.delay) { 70 | return 0; 71 | } 72 | let destPos = this.dest << 3; 73 | if (this.position > destPos) { 74 | return -1; 75 | } else if (this.position < destPos) { 76 | return 1; 77 | } else { 78 | return 0; 79 | } 80 | } 81 | 82 | setDest(dest) { 83 | assert(!(dest & ~0x07)) 84 | this.dest = dest; 85 | } 86 | 87 | getColor() { 88 | assert(this.ns); 89 | let color = this.color; 90 | color = this.ns.PADDLE_DEST.set(color, this.dest); 91 | return color; 92 | } 93 | 94 | nextColor(isLeft) { 95 | let color = this.getColor(); 96 | if (this.isMotionCycle()) { 97 | color = this.ns.PADDLE_POSITION.set(color, 98 | this.position + this.getDY(isLeft)); 99 | } 100 | color = this.ns.DECIMATOR.set(color, !this.decimator); 101 | return color; 102 | } 103 | 104 | // This is the namespace of the source color, and thus of getColor and 105 | // nextColor. 106 | getNamespace() { 107 | return this.ns; 108 | } 109 | } 110 | 111 | window.PaddleState = PaddleState; 112 | })(); 113 | -------------------------------------------------------------------------------- /pong.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* 3 | The things that need to scale up for a larger ball are: 4 | BUFFER_X_DEPTH_COUNTER_BITS, BUFFER_Y_DEPTH_COUNTER_BITS, the BUFFER_[XY]_FLAGs 5 | need to get their MAX and MIN bits back, PADDLE_MOVE_DELAY_COUNTER, the 6 | isCenterRespawn detection and RESPAWN_PHASE_2_FLAG stuff...anything else? Maybe 7 | another shading pixel for the ball's edges?] 8 | */ 9 | 10 | let bm; 11 | (function () { 12 | const HIT_FIRST_SERVE = false; 13 | const BALL_SIZE_BITS = 2; 14 | // We need to keep the depth counter from overflowing, so the buffer can't be 15 | // as deep as 1 << BALL_SIZE_BITS. 16 | const BALL_SIZE = (1 << BALL_SIZE_BITS) - 1; 17 | // Paddle is 6, covers 6 + BALL_SIZE - 1 ball positions. We encode for 8 18 | // paddle destinations in the AI message. There's an extra BALL_SIZE - 1 19 | // space at the end where the ball can go but the paddle can't; the space at 20 | // the top is already counted. 21 | const DESIRED_BALL_AREA_HEIGHT = (6 + BALL_SIZE - 1) * 8 + BALL_SIZE - 1; 22 | // Add a little extra width to make the AI a bit more competitive. 23 | const EXTRA_WIDTH_UNITS = 1 24 | const DESIRED_BALL_AREA_WIDTH = 25 | 6 * (EXTRA_WIDTH_UNITS + Math.floor((DESIRED_BALL_AREA_HEIGHT + 5) / 6)) 26 | - BALL_SIZE + 1; 27 | // Game area excludes troughs/paddles. We want the ball travel from paddle to 28 | // paddle to be 1 mod 6 [see getNewAIMessage for why], where a 29 | // paddle-to-paddle distance of BALL_SIZE would give ball travel of 0. 30 | assert((DESIRED_BALL_AREA_WIDTH - BALL_SIZE) % 6 == 1); 31 | 32 | const GAME_OVER_SCORE = 10; 33 | let nsBall, nsWall, nsPaddle, nsScoreboard; 34 | let nsBackground, nsGlobal, nsNonbackground; 35 | let isWall, isBackground, isBall, isRespawn, isTrough, isPaddle; 36 | let isPaddleBuffer, isBallInPaddleBuffer, isInPaddleBufferRegion; 37 | let isBallMotionCycleHelper; 38 | let isPaddleMotionCycleHelper, isPaddleBufferMotionCycleHelper; 39 | let isScoreboard; 40 | let isSendingLeftMessageDown, isSendingRightMessageDown; 41 | let copySets = {}; 42 | const BUFFER_X_DEPTH_COUNTER_BITS = BALL_SIZE_BITS; 43 | const BUFFER_Y_DEPTH_COUNTER_BITS = BALL_SIZE_BITS; 44 | const BUFFER_SIZE = BALL_SIZE; 45 | const SCOREBOARD_HEIGHT = 12; // 10x15 looks good 46 | const SCOREBOARD_WIDTH = 18; 47 | const RESPAWN_INDEX = 5; 48 | const RESPAWN_DOWN = 1; 49 | // TODO: This is a hard-coded value because I'm lazy. If you change the game 50 | // dimensions, you'll have to code in the new value. 51 | const RESPAWN_PADDLE_DEST = 8; 52 | 53 | // This is assumed throughout the file, in figuring out buffer bits and ball 54 | // pixels. 55 | assert(BALL_SIZE === 3); 56 | 57 | let ballAreaWidth; 58 | let ballAreaHeight; 59 | let paddleToPaddleBallDistance; 60 | let topWallToBottomWallBallDistance; 61 | 62 | function initBitManager(obviousColors) { 63 | nsGlobal = new Namespace(); 64 | canvas.ns = nsGlobal; 65 | bm = new BitManager(nsGlobal); 66 | // Bits are 0xAABBGGRR because of endianness; TODO: Make endian-independent. 67 | 68 | nsGlobal.declare('IS_NOT_BACKGROUND', 1, 31); 69 | nsGlobal.setSubspaceMask('IS_NOT_BACKGROUND'); 70 | nsBackground = nsGlobal.declareSubspace('BACKGROUND', 0); 71 | nsNonbackground = nsGlobal.declareSubspace('NONBACKGROUND', 72 | 'IS_NOT_BACKGROUND'); 73 | 74 | nsNonbackground.declare('ID_0', 1, 15); 75 | nsNonbackground.declare('ID_1', 1, 7); 76 | 77 | // Sentinel bits that determine type: 78 | nsNonbackground.alias('WALL_FLAG', 'ID_0'); 79 | nsNonbackground.alias('PADDLE_FLAG', 'ID_1'); 80 | nsNonbackground.combine('ID_BITS', ['ID_0', 'ID_1']); 81 | nsNonbackground.alias('BALL_FLAG', 'ID_BITS'); 82 | 83 | nsNonbackground.declare('FULL_ALPHA', 2, 29); 84 | if (obviousColors) { 85 | nsBackground.declare('FULL_ALPHA', 3, 28); 86 | nsBackground.alias('BASIC_BACKGROUND', 'FULL_ALPHA'); 87 | } else { 88 | nsBackground.alloc('BASIC_BACKGROUND', 0, 0); 89 | } 90 | 91 | nsNonbackground.setSubspaceMask('ID_BITS'); 92 | nsBall = nsNonbackground.declareSubspace('BALL', 'BALL_FLAG'); 93 | nsWall = nsNonbackground.declareSubspace('WALL', 'WALL_FLAG'); 94 | nsPaddle = nsNonbackground.declareSubspace('PADDLE', 'PADDLE_FLAG'); 95 | nsScoreboard = nsNonbackground.declareSubspace('SCOREBOARD', 0); 96 | 97 | // Message fields shared by wall and background 98 | nsWall.alloc('MESSAGE_R_NOT_L', 1); 99 | nsBackground.alloc('MESSAGE_R_NOT_L', 1); 100 | if (obviousColors) { 101 | nsWall.declare('MESSAGE_PRESENT', 1, 14); 102 | nsBackground.declare('MESSAGE_PRESENT', 1, 15); 103 | } else { 104 | nsWall.alloc('MESSAGE_PRESENT', 1); 105 | nsBackground.alloc('MESSAGE_PRESENT', 1); 106 | } 107 | nsWall.combine('RESPAWN_MESSAGE_BITS', 108 | ['MESSAGE_PRESENT', 'MESSAGE_R_NOT_L']); 109 | 110 | // Used only by the ball. 111 | if (obviousColors) { 112 | nsBall.declare('DECIMATOR', 1, 22); 113 | } else { 114 | nsBall.declare('DECIMATOR', 1, 24); 115 | } 116 | nsBall.alloc('PADDLE_POSITION', 6); 117 | nsBall.alloc('PADDLE_DEST', 3); 118 | nsBall.alloc('MOVE_INDEX', 3); 119 | nsBall.alloc('BUFFER_X_DEPTH_COUNTER', BUFFER_X_DEPTH_COUNTER_BITS); 120 | nsBall.alloc('BUFFER_Y_DEPTH_COUNTER', BUFFER_Y_DEPTH_COUNTER_BITS); 121 | nsBall.alloc('MOVE_STATE', 2); 122 | nsBall.alloc('MOVE_R_NOT_L', 1); 123 | nsBall.alloc('MOVE_D_NOT_U', 1); 124 | nsBall.alloc('PADDLE_PIXEL', 1); 125 | nsBall.alloc('PADDLE_BUFFER_FLAG', 1); 126 | copySets.PADDLE_BALL_BITS = 127 | ['PADDLE_POSITION', 'PADDLE_DEST', 'PADDLE_PIXEL', 'PADDLE_BUFFER_FLAG'] 128 | nsBall.combine('PADDLE_BALL_BITS', copySets.PADDLE_BALL_BITS); 129 | 130 | nsWall.alloc('LISTEN_DOWN', 1); 131 | nsWall.alloc('LISTEN_UP_FOR_L', 1); 132 | nsWall.alloc('LISTEN_UP_FOR_R', 1); 133 | if (obviousColors) { 134 | nsWall.alloc('LISTEN_RIGHT_FOR_R', 1); 135 | nsWall.alloc('LISTEN_LEFT_FOR_L', 1); 136 | nsWall.alloc('LISTEN_LEFT', 1); 137 | nsWall.alloc('LISTEN_RIGHT', 1); 138 | } else { 139 | nsWall.declare('LISTEN_RIGHT_FOR_R', 1, 17); 140 | nsWall.declare('LISTEN_LEFT_FOR_L', 1, 18); 141 | nsWall.declare('LISTEN_LEFT', 1, 19); 142 | nsWall.declare('LISTEN_RIGHT', 1, 20); 143 | } 144 | nsWall.alloc('TALK_DOWN_FOR_L', 1); 145 | nsWall.alloc('TALK_DOWN_FOR_R', 1); 146 | nsWall.alloc('SIDE_WALL_FLAG', 1); 147 | if (obviousColors) { 148 | nsWall.alloc('LISTEN_SIDE_FOR_GAME_OVER', 1); 149 | } else { 150 | nsWall.declare('LISTEN_SIDE_FOR_GAME_OVER', 1, 16); 151 | } 152 | 153 | // TODO: We can do without this, by figuring out which respawn pixel we're 154 | // on and watching the message hit the center one, in a 3x3 ball. 155 | nsBackground.alloc('RESPAWN_PHASE_2_FLAG', 1); 156 | nsBackground.alloc('TROUGH_FLAG', 1); 157 | if (obviousColors) { 158 | nsBackground.declare('BALL_MISS_FLAG', 1, 13); 159 | nsBackground.declare('RESPAWN_FLAG', 1, 6); 160 | } else { 161 | nsBackground.alloc('BALL_MISS_FLAG', 1); 162 | nsBackground.alloc('RESPAWN_FLAG', 1); 163 | } 164 | 165 | // Used by background and ball [since the ball has to replace the background 166 | // bits it runs over]. 167 | nsBall.alloc('BUFFER_X_MIN_FLAG', 1); 168 | nsBall.alloc('BUFFER_X_MAX_FLAG', 1); 169 | nsBall.alloc('BUFFER_Y_FLAG', 1); 170 | nsBackground.alloc('BUFFER_X_MIN_FLAG', 1); 171 | nsBackground.alloc('BUFFER_X_MAX_FLAG', 1); 172 | nsBackground.alloc('BUFFER_Y_FLAG', 1); 173 | 174 | nsBall.alloc('RESPAWN_FLAG', 1); 175 | 176 | // Paddle fields 177 | nsPaddle.declare('BRIGHT_COLOR_0', 1, 23); 178 | nsPaddle.declare('BRIGHT_COLOR_1', 2, 12); 179 | nsPaddle.declare('BRIGHT_COLOR_2', 1, 6); 180 | if (obviousColors) { 181 | nsPaddle.declare('DECIMATOR', 1, 22); 182 | } else { 183 | nsPaddle.declare('DECIMATOR', 1, 24); 184 | } 185 | nsPaddle.alloc('PADDLE_POSITION', 6); 186 | nsPaddle.alloc('PADDLE_DEST', 3); 187 | if (obviousColors) { 188 | nsPaddle.declare('PADDLE_PIXEL', 1, 14); 189 | } else { 190 | nsPaddle.declare('PADDLE_PIXEL', 1, 18); 191 | } 192 | 193 | // Background fields for paddle 194 | if (obviousColors) { 195 | nsBackground.declare('DECIMATOR', 1, 22); 196 | } else { 197 | nsBackground.declare('DECIMATOR', 1, 24); 198 | } 199 | nsBackground.alloc('PADDLE_POSITION', 6); 200 | nsBackground.alloc('PADDLE_DEST', 3); 201 | nsBackground.alloc('PADDLE_MOVE_DELAY_COUNTER', 3); 202 | if (obviousColors) { 203 | nsBackground.declare('PADDLE_PIXEL', 1, 14); 204 | } else { 205 | nsBackground.alloc('PADDLE_PIXEL', 1); 206 | } 207 | nsBackground.alloc('PADDLE_BUFFER_FLAG', 1); 208 | // We don't copy decimator because we always flip it. 209 | nsBackground.combine( 210 | 'PADDLE_BACKGROUND_BITS', 211 | ['PADDLE_POSITION', 'PADDLE_DEST', 'PADDLE_MOVE_DELAY_COUNTER', 212 | 'PADDLE_PIXEL', 'PADDLE_BUFFER_FLAG']); 213 | 214 | // Background-only AI message fields 215 | nsBackground.alloc('MESSAGE_H_NOT_V', 1); 216 | nsBackground.alloc('MESSAGE_PADDLE_POSITION', 3); 217 | nsBackground.combine('ALL_MESSAGE_BITS', 218 | ['MESSAGE_H_NOT_V', 'MESSAGE_R_NOT_L', 219 | 'MESSAGE_PADDLE_POSITION', 'MESSAGE_PRESENT']); 220 | 221 | isWall = getHasValueFunction(bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 222 | nsNonbackground.ID_BITS.getMask()]), 223 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 224 | nsNonbackground.WALL_FLAG.getMask()])); 225 | isBackground = getHasValueFunction(nsGlobal.IS_NOT_BACKGROUND.getMask(), 0); 226 | isBall = getHasValueFunction(bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 227 | nsNonbackground.ID_BITS.getMask()]), 228 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 229 | nsNonbackground.BALL_FLAG.getMask()])); 230 | isBallMotionCycleHelper = getHasValueFunction(nsBall.DECIMATOR.getMask(), 231 | nsBall.DECIMATOR.getMask()); 232 | isPaddleMotionCycleHelper = 233 | getHasValueFunction(nsPaddle.DECIMATOR.getMask(), 0); 234 | isPaddleBufferMotionCycleHelper = 235 | getHasValueFunction(nsBackground.DECIMATOR.getMask(), 0); 236 | isRespawn = getHasValueFunction( 237 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 238 | nsBackground.RESPAWN_FLAG.getMask()]), 239 | nsBackground.RESPAWN_FLAG.getMask()); 240 | isTrough = getHasValueFunction( 241 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 242 | nsBackground.TROUGH_FLAG.getMask()]), 243 | nsBackground.TROUGH_FLAG.getMask()); 244 | isPaddleBuffer = getHasValueFunction( 245 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 246 | nsBackground.PADDLE_BUFFER_FLAG.getMask()]), 247 | nsBackground.PADDLE_BUFFER_FLAG.getMask()); 248 | isBallInPaddleBuffer = getHasValueFunction( 249 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 250 | nsNonbackground.ID_BITS.getMask(), 251 | nsBall.PADDLE_BUFFER_FLAG.getMask()]), 252 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 253 | nsNonbackground.BALL_FLAG.getMask(), 254 | nsBall.PADDLE_BUFFER_FLAG.getMask()])); 255 | isInPaddleBufferRegion = 256 | d => (isPaddleBuffer(d) || isBallInPaddleBuffer(d)); 257 | isPaddle = getHasValueFunction( 258 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 259 | nsNonbackground.ID_BITS.getMask()]), 260 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 261 | nsNonbackground.PADDLE_FLAG.getMask()])); 262 | isSendingLeftMessageDown = 263 | getHasValueFunction(bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 264 | nsNonbackground.ID_BITS.getMask(), 265 | nsWall.TALK_DOWN_FOR_L.getMask(), 266 | nsWall.MESSAGE_PRESENT.getMask(), 267 | nsWall.MESSAGE_R_NOT_L.getMask()]), 268 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 269 | nsNonbackground.WALL_FLAG.getMask(), 270 | nsWall.TALK_DOWN_FOR_L.getMask(), 271 | nsWall.MESSAGE_PRESENT.getMask()])); 272 | isSendingRightMessageDown = 273 | getHasValueFunction(bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 274 | nsNonbackground.ID_BITS.getMask(), 275 | nsWall.TALK_DOWN_FOR_R.getMask(), 276 | nsWall.MESSAGE_PRESENT.getMask(), 277 | nsWall.MESSAGE_R_NOT_L.getMask()]), 278 | bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 279 | nsNonbackground.WALL_FLAG.getMask(), 280 | nsWall.TALK_DOWN_FOR_R.getMask(), 281 | nsWall.MESSAGE_PRESENT.getMask(), 282 | nsWall.MESSAGE_R_NOT_L.getMask()])); 283 | isScoreboard = 284 | getHasValueFunction(bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 285 | nsNonbackground.ID_BITS.getMask()]), 286 | nsGlobal.IS_NOT_BACKGROUND.getMask()); 287 | initScoreboard(nsScoreboard, nsGlobal.IS_NOT_BACKGROUND.getMask(), 288 | nsNonbackground.FULL_ALPHA, isScoreboard, 289 | isSendingMessageDown, isSignallingGameOver, obviousColors); 290 | PaddleState.init(nsPaddle, nsBall, nsBackground, isPaddle, 291 | isBallInPaddleBuffer, isPaddleBuffer); 292 | nsGlobal.dumpStatus(); 293 | } 294 | 295 | function isSendingMessageDown(c) { 296 | return isSendingLeftMessageDown(c) || isSendingRightMessageDown(c); 297 | } 298 | 299 | function isSignallingGameOver(c) { 300 | return isWall(c) && nsWall.LISTEN_SIDE_FOR_GAME_OVER.isSet(c) && 301 | nsWall.MESSAGE_PRESENT.isSet(c); 302 | } 303 | 304 | function isBallMotionCycle(c) { 305 | assert(isBall(c)); 306 | return isBallMotionCycleHelper(c); 307 | } 308 | 309 | function isPaddleMotionCycleGeneral(c) { 310 | if (isPaddle(c)) { 311 | return isPaddleMotionCycleHelper(c); 312 | } 313 | if (isBallInPaddleBuffer(c)) { 314 | return !isBallMotionCycle(c); 315 | } 316 | assert(isPaddleBuffer(c)); 317 | return isPaddleBufferMotionCycleHelper(c); 318 | } 319 | 320 | function isPaddleMotionCycle(c) { 321 | assert(isPaddle(c)); 322 | return isPaddleMotionCycleHelper(c); 323 | } 324 | 325 | function isCenterRespawn(data) { 326 | assert(_.isArray(data)); 327 | return _.every(data, isRespawn); 328 | } 329 | 330 | function sourceDirectionFromIndex(i) { 331 | let dirBits; 332 | switch (i) { 333 | case 0: 334 | return { dX: 1, dY: 1 }; 335 | case 1: 336 | return { dX: 0, dY: 1 }; 337 | case 2: 338 | return { dX: -1, dY: 1 }; 339 | case 3: 340 | return { dX: 1, dY: 0 }; 341 | case 4: 342 | return { dX: 0, dY: 0 }; 343 | case 5: 344 | return { dX: -1, dY: 0 }; 345 | case 6: 346 | return { dX: 1, dY: -1 }; 347 | case 7: 348 | return { dX: 0, dY: -1 }; 349 | case 8: 350 | return { dX: -1, dY: -1 }; 351 | default: assert(false); 352 | } 353 | } 354 | 355 | function initPong(c, originX, originY, width, height, obviousColors) { 356 | const gameOriginX = originX; 357 | const gameOriginY = originY + SCOREBOARD_HEIGHT; 358 | const gameWidth = width; 359 | 360 | // width must be at least one plus BUFFER_SIZE greater than the height for 361 | // the AI message to be safe, otherwise a corner-sourced message might not 362 | // reach all pixels of the paddle, leading to it tearing in half. 363 | const gameHeight = height - SCOREBOARD_HEIGHT; 364 | assert(gameWidth + 1 + BUFFER_SIZE >= gameHeight); 365 | 366 | const insideWallOriginX = gameOriginX + 1; 367 | const insideWallOriginY = gameOriginY + 1; 368 | const insideWallWidth = gameWidth - 2; 369 | const insideWallHeight = gameHeight - 2; 370 | const ballAreaOriginX = insideWallOriginX + 1; // skip the trough 371 | const ballAreaOriginY = insideWallOriginY; 372 | // 2 for trough/paddle 373 | ballAreaWidth = insideWallWidth - 2; 374 | ballAreaHeight = insideWallHeight; 375 | const gameHalfHeight = Math.floor(gameHeight / 2); 376 | paddleToPaddleBallDistance = ballAreaWidth - BALL_SIZE; 377 | 378 | // See getNewAIMessage. 379 | assert(paddleToPaddleBallDistance % 6 === 1); 380 | 381 | topWallToBottomWallBallDistance = ballAreaHeight - BALL_SIZE; 382 | 383 | const pixelEncoding = [0, 1, 0, 0, 0, 1, 1, 1, 0, 1]; 384 | function drawPaddle(c, isLeft, topInPaddleCoords, dest) { 385 | assert(_.isBoolean(isLeft)); 386 | assert(_.isNumber(topInPaddleCoords)); 387 | assert(topInPaddleCoords >= 0); 388 | assert(topInPaddleCoords + 10 <= insideWallHeight); 389 | assert(_.isNumber(dest) && dest >= 0 && dest < 8); 390 | let paddleBaseColor = bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 391 | nsNonbackground.PADDLE_FLAG.getMask(), 392 | nsPaddle.BRIGHT_COLOR_0.getMask(), 393 | nsPaddle.BRIGHT_COLOR_1.getMask(), 394 | nsPaddle.BRIGHT_COLOR_2.getMask(),]); 395 | let bufferFlag = isLeft ? nsBackground.BUFFER_X_MIN_FLAG.getMask() : 396 | nsBackground.BUFFER_X_MAX_FLAG.getMask(); 397 | let bufferBaseColor = bm.or([nsBackground.BASIC_BACKGROUND.getMask(), 398 | nsBackground.PADDLE_BUFFER_FLAG.getMask(), 399 | bufferFlag]); 400 | paddleBaseColor = 401 | nsPaddle.PADDLE_POSITION.set(paddleBaseColor, topInPaddleCoords); 402 | bufferBaseColor = 403 | nsBackground.PADDLE_POSITION.set(bufferBaseColor, topInPaddleCoords); 404 | paddleBaseColor = nsPaddle.PADDLE_DEST.set(paddleBaseColor, dest); 405 | bufferBaseColor = nsBackground.PADDLE_DEST.set(bufferBaseColor, dest); 406 | for (let pixel = 0; pixel < 10; ++pixel) { 407 | let paddleColor = 408 | nsPaddle.PADDLE_PIXEL.set(paddleBaseColor, pixelEncoding[pixel]); 409 | let bufferColor = 410 | nsBackground.PADDLE_PIXEL.set(bufferBaseColor, pixelEncoding[pixel]); 411 | let currentHeight = topInPaddleCoords + pixel; 412 | if (currentHeight < BUFFER_SIZE || 413 | currentHeight >= insideWallHeight - BUFFER_SIZE) { 414 | bufferColor = nsBackground.BUFFER_Y_FLAG.setMask(bufferColor, true); 415 | } 416 | // Draw 10 rows of buffer, but 6 paddle pixels. 417 | c.fillRect(bufferColor, 418 | isLeft ? ballAreaOriginX 419 | : ballAreaOriginX + ballAreaWidth - BUFFER_SIZE, 420 | topInPaddleCoords + ballAreaOriginY + pixel, 421 | BUFFER_SIZE, 1); 422 | if (pixel > 1 && pixel < 8) { 423 | c.fillRect(paddleColor, 424 | isLeft ? insideWallOriginX 425 | : insideWallOriginX + insideWallWidth - 1, 426 | topInPaddleCoords + insideWallOriginY + pixel, 1, 1); 427 | } 428 | } 429 | } 430 | 431 | initBitManager(obviousColors); 432 | 433 | let leftScoreboardRightEdge = originX + SCOREBOARD_WIDTH - 1; 434 | let rightScoreboardLeftEdge = originX + width - SCOREBOARD_WIDTH; 435 | let leftRespawnDownPathX = leftScoreboardRightEdge - 2; 436 | let rightRespawnDownPathX = rightScoreboardLeftEdge + 2; 437 | 438 | // background 439 | let background = nsBackground.BASIC_BACKGROUND.getMask(); 440 | c.fillRect(background, 0, 0, canvas.width, canvas.height); 441 | 442 | let topWallCenterX = Math.ceil(gameWidth / 2); 443 | 444 | // respawn squares 445 | c.orRect(nsBackground.RESPAWN_FLAG.getMask(), 446 | leftRespawnDownPathX - 1, gameOriginY + gameHalfHeight - 1, 447 | BALL_SIZE, BALL_SIZE); 448 | c.orRect(nsBackground.RESPAWN_FLAG.getMask(), 449 | rightRespawnDownPathX - 1, gameOriginY + gameHalfHeight - 1, 450 | BALL_SIZE, BALL_SIZE); 451 | 452 | // initial message to create the first ball 453 | c.orRect(bm.or([nsBackground.MESSAGE_PRESENT.getMask(), 454 | nsBackground.MESSAGE_R_NOT_L.getMask()]), 455 | leftRespawnDownPathX, gameOriginY + gameHalfHeight - 2, 1, 1); 456 | 457 | // walls 458 | let color = bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 459 | nsNonbackground.WALL_FLAG.getMask(), 460 | nsNonbackground.FULL_ALPHA.getMask()]); 461 | c.strokeRect(color, originX, originY, 462 | SCOREBOARD_WIDTH, SCOREBOARD_HEIGHT + 1); 463 | c.strokeRect(color, originX + width - SCOREBOARD_WIDTH, originY, 464 | SCOREBOARD_WIDTH, SCOREBOARD_HEIGHT + 1); 465 | c.strokeRect(color, originX, originY, 466 | width, SCOREBOARD_HEIGHT + 1); 467 | c.strokeRect(color, gameOriginX, gameOriginY, 468 | gameWidth, gameHeight); 469 | c.orRect(nsWall.SIDE_WALL_FLAG.getMask(), 470 | gameOriginX, gameOriginY + 1, 1, gameHeight - 2); 471 | c.orRect(nsWall.LISTEN_DOWN.getMask(), 472 | originX, originY, 1, height - 1); 473 | c.orRect(nsWall.SIDE_WALL_FLAG.getMask(), 474 | gameOriginX + gameWidth - 1, gameOriginY + 1, 1, gameHeight - 2); 475 | c.orRect(nsWall.LISTEN_DOWN.getMask(), 476 | originX + width - 1, originY, 1, height - 1); 477 | 478 | c.orRect(nsWall.LISTEN_LEFT.getMask(), 479 | originX + 1, originY, rightScoreboardLeftEdge - originX + 1, 1); 480 | c.orRect(nsWall.LISTEN_RIGHT.getMask(), 481 | leftScoreboardRightEdge - 1, originY, 482 | width - SCOREBOARD_WIDTH + 1, 1); 483 | 484 | c.orRect(nsWall.TALK_DOWN_FOR_L.getMask(), 485 | leftScoreboardRightEdge - 1, originY, 1, 1); 486 | c.orRect(nsWall.TALK_DOWN_FOR_R.getMask(), 487 | rightScoreboardLeftEdge + 1, originY, 1, 1); 488 | c.orRect(nsWall.LISTEN_UP_FOR_L.getMask(), 489 | leftScoreboardRightEdge - 1, originY + SCOREBOARD_HEIGHT, 1, 1); 490 | c.orRect(nsWall.LISTEN_UP_FOR_R.getMask(), 491 | rightScoreboardLeftEdge + 1, originY + SCOREBOARD_HEIGHT, 1, 1); 492 | 493 | c.orRect(nsWall.LISTEN_LEFT_FOR_L.getMask(), 494 | leftScoreboardRightEdge, originY + SCOREBOARD_HEIGHT, 495 | rightRespawnDownPathX - leftScoreboardRightEdge + 1, 1); 496 | c.orRect(nsWall.LISTEN_RIGHT_FOR_R.getMask(), 497 | leftRespawnDownPathX, originY + SCOREBOARD_HEIGHT, 498 | rightScoreboardLeftEdge - leftRespawnDownPathX + 1, 1); 499 | c.orRect(nsWall.TALK_DOWN_FOR_R.getMask(), 500 | leftRespawnDownPathX, originY + SCOREBOARD_HEIGHT, 1, 1); 501 | c.orRect(nsWall.TALK_DOWN_FOR_L.getMask(), 502 | rightRespawnDownPathX, originY + SCOREBOARD_HEIGHT, 1, 1); 503 | 504 | c.orRect(nsWall.LISTEN_SIDE_FOR_GAME_OVER.getMask(), 505 | leftScoreboardRightEdge, originY + SCOREBOARD_HEIGHT / 2, 1, 1); 506 | c.orRect(nsWall.LISTEN_SIDE_FOR_GAME_OVER.getMask(), 507 | rightScoreboardLeftEdge, originY + SCOREBOARD_HEIGHT / 2, 1, 1); 508 | 509 | // buffer regions 510 | let bufferY = nsBackground.BUFFER_Y_FLAG.getMask(); 511 | c.orRect(nsBackground.BUFFER_X_MIN_FLAG.getMask(), 512 | ballAreaOriginX, ballAreaOriginY, 513 | BUFFER_SIZE, ballAreaHeight); 514 | c.orRect(nsBackground.BUFFER_X_MAX_FLAG.getMask(), 515 | ballAreaOriginX + ballAreaWidth - BUFFER_SIZE, 516 | ballAreaOriginY, 517 | BUFFER_SIZE, ballAreaHeight); 518 | c.orRect(bufferY, 519 | ballAreaOriginX, ballAreaOriginY, 520 | ballAreaWidth, BUFFER_SIZE); 521 | c.orRect(bufferY, 522 | ballAreaOriginX, 523 | ballAreaOriginY + ballAreaHeight - BUFFER_SIZE, 524 | ballAreaWidth, BUFFER_SIZE); 525 | 526 | 527 | // trough lines 528 | let trough = bm.or([nsBackground.TROUGH_FLAG.getMask(), background]); 529 | c.fillRect(trough, insideWallOriginX, insideWallOriginY, 1, 530 | insideWallHeight); 531 | c.fillRect(trough, insideWallOriginX + insideWallWidth - 1, 532 | insideWallOriginY, 1, insideWallHeight); 533 | 534 | if (HIT_FIRST_SERVE) { 535 | drawPaddle(c, true, 42, 4); 536 | drawPaddle(c, false, 48, 7); 537 | } else { 538 | drawPaddle(c, true, 40, 5); 539 | drawPaddle(c, false, 48, 6); 540 | } 541 | drawScoreboard(c, originX + 1, originY + 1, 542 | SCOREBOARD_WIDTH - 2, SCOREBOARD_HEIGHT - 1); 543 | drawScoreboard(c, rightScoreboardLeftEdge + 1, originY + 1, 544 | SCOREBOARD_WIDTH - 2, SCOREBOARD_HEIGHT - 1); 545 | drawGameOver(c, leftScoreboardRightEdge + 1, originY + 1, 546 | rightScoreboardLeftEdge - leftScoreboardRightEdge - 1, 547 | SCOREBOARD_HEIGHT - 1); 548 | } 549 | 550 | function getBufferBits(data, bs) { 551 | function testBounds(lower, current, higher, flag, 552 | bsDepth, bsDir) { 553 | assert(BUFFER_SIZE === 3); 554 | if (isWall(lower) || isTrough(lower) || isPaddle(lower)) { 555 | return 'min'; 556 | } 557 | if (isWall(higher) || isTrough(higher) || isPaddle(higher)) { 558 | return 'max'; 559 | } 560 | // Beyond this line, higher and lower are either empty background, buffer, 561 | // or ball, no trough or wall. 562 | let bgH = isBackground(higher); 563 | let bgL = isBackground(lower); 564 | if ((bgH && !nsBackground[flag].get(higher)) || 565 | (!bgH && !nsBall[flag].get(higher))) { 566 | return 'min'; 567 | } 568 | if ((bgL && !nsBackground[flag].get(lower)) || 569 | (!bgL && !nsBall[flag].get(lower))) { 570 | return 'max'; 571 | } 572 | // The only ball pixels that land on the middle buffer cell are: 573 | // 1) the pixel that just bounced off the wall; 574 | // 2) the leading pixel on its way in; 575 | // 3) the second pixel on its way in. 576 | if (bsDepth === BUFFER_SIZE) { 577 | return bsDir > 0 ? 'min' : 'max'; 578 | } 579 | return bsDir > 0 ? 'max' : 'min'; 580 | } 581 | 582 | // bs is the ball which is going to land where we are. 583 | // If we're in a buffer of any kind, we need to know which, to be able to 584 | // tell if we need to increment or decrement our depth counters, bounce, 585 | // etc. 586 | let current = data[4]; 587 | let bufferXMin, bufferXMax; 588 | let bufferY; 589 | let paddleBuffer; 590 | if (isBall(current)) { 591 | bufferXMin = nsBall.BUFFER_X_MIN_FLAG.get(current); 592 | bufferXMax = nsBall.BUFFER_X_MAX_FLAG.get(current); 593 | bufferY = nsBall.BUFFER_Y_FLAG.get(current); 594 | paddleBuffer = nsBall.PADDLE_BUFFER_FLAG.get(current); 595 | } else { 596 | assert(isBackground(current)); 597 | bufferXMin = nsBackground.BUFFER_X_MIN_FLAG.get(current); 598 | bufferXMax = nsBackground.BUFFER_X_MAX_FLAG.get(current); 599 | bufferY = nsBackground.BUFFER_Y_FLAG.get(current); 600 | paddleBuffer = nsBackground.PADDLE_BUFFER_FLAG.get(current); 601 | } 602 | let bufferYDir = null; 603 | if (bufferY) { 604 | bufferYDir = testBounds(data[1], current, data[7], 'BUFFER_Y_FLAG', 605 | bs.depthY, bs.down); 606 | } 607 | return { 608 | xMin: bufferXMin, 609 | yMin: bufferYDir === 'min', 610 | xMax: bufferXMax, 611 | yMax: bufferYDir === 'max', 612 | paddleBuffer: paddleBuffer 613 | }; 614 | } 615 | 616 | function handleWall(data, x, y) { 617 | const current = data[4]; 618 | 619 | function getMessageFrom(d) { 620 | var next = nsWall.MESSAGE_PRESENT.set(current, 1); 621 | var rNotL = nsWall.MESSAGE_R_NOT_L.get(d); 622 | return nsWall.MESSAGE_R_NOT_L.set(next, rNotL); 623 | } 624 | 625 | function getMessageFromScoreboard(rNotL) { 626 | var next = nsWall.MESSAGE_PRESENT.set(current, 1); 627 | return nsWall.MESSAGE_R_NOT_L.setMask(next, rNotL); 628 | } 629 | 630 | if (nsWall.LISTEN_DOWN.isSet(current) && 631 | nsWall.MESSAGE_PRESENT.isSet(data[7])) { 632 | return getMessageFrom(data[7]); 633 | } 634 | if (nsWall.LISTEN_RIGHT.isSet(current) && 635 | nsWall.MESSAGE_PRESENT.isSet(data[5]) && 636 | !nsWall.MESSAGE_R_NOT_L.isSet(data[5])) { 637 | return getMessageFrom(data[5]); 638 | } 639 | if (nsWall.LISTEN_LEFT.isSet(current) && 640 | nsWall.MESSAGE_PRESENT.isSet(data[3]) && 641 | nsWall.MESSAGE_R_NOT_L.isSet(data[3])) { 642 | return getMessageFrom(data[3]); 643 | } 644 | if (nsWall.LISTEN_RIGHT_FOR_R.isSet(current) && 645 | nsWall.MESSAGE_PRESENT.isSet(data[5]) && 646 | nsWall.MESSAGE_R_NOT_L.isSet(data[5])) { 647 | return getMessageFrom(data[5]); 648 | } 649 | if (nsWall.LISTEN_LEFT_FOR_L.isSet(current) && 650 | nsWall.MESSAGE_PRESENT.isSet(data[3]) && 651 | !nsWall.MESSAGE_R_NOT_L.isSet(data[3])) { 652 | return getMessageFrom(data[3]); 653 | } 654 | if (nsWall.LISTEN_UP_FOR_L.isSet(current)) { 655 | if (isWall(data[1]) && 656 | nsWall.MESSAGE_PRESENT.isSet(data[1]) && 657 | !nsWall.MESSAGE_R_NOT_L.isSet(data[1])) { 658 | return getMessageFrom(data[1]); 659 | } 660 | if (isScoreboard(data[1]) && 661 | nsScoreboard.SCOREBOARD_CHANGED.isSet(data[1]) && 662 | (nsScoreboard.SCOREBOARD_BITS.get(data[1]) < GAME_OVER_SCORE)) { 663 | return getMessageFromScoreboard(false); 664 | } 665 | } 666 | if (nsWall.LISTEN_UP_FOR_R.isSet(current)) { 667 | if (isWall(data[1]) && 668 | nsWall.MESSAGE_PRESENT.isSet(data[1]) && 669 | nsWall.MESSAGE_R_NOT_L.isSet(data[1])) { 670 | return getMessageFrom(data[1]); 671 | } 672 | if (isScoreboard(data[1]) && 673 | nsScoreboard.SCOREBOARD_CHANGED.isSet(data[1]) && 674 | (nsScoreboard.SCOREBOARD_BITS.get(data[1]) < GAME_OVER_SCORE)) { 675 | return getMessageFromScoreboard(true); 676 | } 677 | } 678 | if (nsWall.LISTEN_SIDE_FOR_GAME_OVER.isSet(current)) { 679 | for (let value of [data[3], data[5]]) { 680 | if (isScoreboard(value) && 681 | nsScoreboard.SCOREBOARD_CHANGED.isSet(value) && 682 | (nsScoreboard.SCOREBOARD_BITS.get(value) === GAME_OVER_SCORE)) { 683 | return nsWall.MESSAGE_PRESENT.set(current, 1); 684 | } 685 | } 686 | } 687 | if (nsWall.SIDE_WALL_FLAG.isSet(current)) { 688 | if (isTrough(data[3]) && 689 | nsBackground.BALL_MISS_FLAG.isSet(data[3])) { 690 | return nsWall.MESSAGE_PRESENT.set(current, 1); 691 | } else if (isTrough(data[5]) && 692 | nsBackground.BALL_MISS_FLAG.isSet(data[5])) { 693 | var next = nsWall.MESSAGE_PRESENT.set(current, 1); 694 | return nsWall.MESSAGE_R_NOT_L.set(next, 1); 695 | } 696 | } 697 | return nsWall.RESPAWN_MESSAGE_BITS.set(current, 0); 698 | } 699 | 700 | function handleTroughAndPaddle(data, x, y) { 701 | let current = data[4]; 702 | let leftBall = false; 703 | if (isTrough(current) && 704 | ((leftBall = _.every([0, 3, 6], i => isBall(data[i]))) || 705 | _.every([2, 5, 8], i => isBall(data[i])))) { 706 | let ballMissedPaddle = 707 | !nsBall.BUFFER_X_DEPTH_COUNTER.get(leftBall ? data[0] : data[2]) 708 | if (ballMissedPaddle) { 709 | return nsBackground.BALL_MISS_FLAG.set(current, 1); 710 | } 711 | } 712 | let newDest; 713 | for (let i of [3, 5]) { 714 | let color = data[i]; 715 | if (isBackground(color) && nsBackground.MESSAGE_PRESENT.isSet(color) && 716 | nsBackground.MESSAGE_R_NOT_L.isSet(color) === (i === 3)) { 717 | newDest = nsBackground.MESSAGE_PADDLE_POSITION.get(color); 718 | } 719 | } 720 | if (isPaddle(current) && !isPaddleMotionCycle(current)) { 721 | let nextColor = 722 | nsPaddle.DECIMATOR.setMask(current, !nsPaddle.DECIMATOR.isSet(current)); 723 | if (newDest !== undefined) { 724 | nextColor = nsPaddle.PADDLE_DEST.set(nextColor, newDest); 725 | } 726 | return nextColor; 727 | } 728 | let isLeft = isBackground(data[5]) || isBall(data[5]); 729 | for (let index of [1, 4, 7]) { 730 | let color = data[index]; 731 | if (isPaddle(color)) { 732 | if (!isPaddleMotionCycle(color)) { 733 | // no need to check any other paddle pixels 734 | break; 735 | } 736 | let ps = new PaddleState(color); 737 | let dY = ps.getDY(isLeft); 738 | if ((index === 1 && dY > 0) || 739 | (index === 4 && dY === 0) || 740 | (index === 7 && dY < 0)) { 741 | let nextColor = ps.nextColor(isLeft); 742 | if (newDest !== undefined) { 743 | nextColor = nsPaddle.PADDLE_DEST.set(nextColor, newDest); 744 | } 745 | return nextColor; 746 | } 747 | } 748 | } 749 | let color = bm.or([nsBackground.TROUGH_FLAG.getMask(), 750 | nsBackground.BASIC_BACKGROUND.getMask()]); 751 | return nsBackground.BALL_MISS_FLAG.set(color, 0); 752 | } 753 | 754 | function handleRespawnMessage(data, x, y) { 755 | let current = data[4]; 756 | let backgroundAbove = isBackground(data[1]); 757 | let wallMessageAbove = isSendingMessageDown(data[1]); 758 | if (isBackground(current) && (backgroundAbove || wallMessageAbove)) { 759 | let activeRespawnMessage = wallMessageAbove || 760 | (backgroundAbove && nsBackground.MESSAGE_PRESENT.isSet(data[1]) && 761 | !nsBackground.MESSAGE_H_NOT_V.isSet(data[1])); 762 | let rightNotL; 763 | if (backgroundAbove) { 764 | rightNotL = nsBackground.MESSAGE_R_NOT_L.isSet(data[1]); 765 | } else { 766 | rightNotL = nsWall.MESSAGE_R_NOT_L.isSet(data[1]); 767 | } 768 | let decimator; 769 | let respawn = isRespawn(current); 770 | if (respawn) { 771 | decimator = nsBackground.DECIMATOR.isSet(current); 772 | } 773 | if (activeRespawnMessage) { 774 | if (isCenterRespawn(data)) { 775 | let color = nsBackground.MESSAGE_R_NOT_L.set(current, rightNotL); 776 | color = nsBackground.RESPAWN_FLAG.set(color, true); 777 | color = nsBackground.RESPAWN_PHASE_2_FLAG.set(color, true); 778 | color = nsBackground.DECIMATOR.setMask(color, !decimator); 779 | return { value: color }; 780 | } else { 781 | let color = nsBackground.MESSAGE_R_NOT_L.setMask(current, rightNotL); 782 | color = nsBackground.MESSAGE_PRESENT.setMask(color, true); 783 | if (respawn) { 784 | color = nsBackground.DECIMATOR.setMask(color, !decimator); 785 | } 786 | return { value: color }; 787 | } 788 | } 789 | } 790 | if (isRespawn(current)) { 791 | let decimator = nsBackground.DECIMATOR.isSet(current); 792 | for (let i in data) { 793 | let d = data[i]; 794 | if (isBackground(d) && nsBackground.RESPAWN_PHASE_2_FLAG.get(d)) { 795 | let rightNotL = nsBackground.MESSAGE_R_NOT_L.get(d); 796 | let color = bm.or([nsGlobal.IS_NOT_BACKGROUND.getMask(), 797 | nsNonbackground.BALL_FLAG.getMask(), 798 | nsNonbackground.FULL_ALPHA.getMask()]); 799 | var bs = BallState.create(nsBall, rightNotL, RESPAWN_DOWN, 800 | RESPAWN_INDEX, 0, color); 801 | let next = bs.getColor(); 802 | next = nsBall.RESPAWN_FLAG.setMask(next, true); 803 | next = nsBall.DECIMATOR.setMask(next, !decimator); 804 | if (i !== "4") { 805 | // turn down the alpha for a decorative dot 806 | next = nsNonbackground.FULL_ALPHA.set(next, 1); 807 | } 808 | return { value: next }; 809 | } 810 | } 811 | } 812 | return null; 813 | } 814 | 815 | // Figure out the relevant paddle pixel for bounce, or return null if the ball 816 | // isn't completely within the paddle buffer region. We have bs, so we know 817 | // that at least one ball pixel is within reach, and we know that data[4] is 818 | // in the paddle buffer region. 819 | function getPaddlePixel(ballDY, data, ballDataColumn, x, y) { 820 | assert(isInPaddleBufferRegion(data[4])); 821 | let d0 = data[ballDataColumn[0]] 822 | let d1 = data[ballDataColumn[1]] 823 | let d2 = data[ballDataColumn[2]] 824 | let bTop = isBall(d0); 825 | let bMid = isBall(d1); 826 | let bBot = isBall(d2); 827 | let ballCurPos; 828 | if (bTop && bMid && bBot) { 829 | ballCurPos = 0; 830 | } else if (bTop && bMid) { 831 | ballCurPos = -1; 832 | } else if (bMid && bBot) { 833 | ballCurPos = 1; 834 | } else if (bTop) { 835 | ballCurPos = -2; 836 | } else if (bBot) { 837 | ballCurPos = 2; 838 | } else { 839 | assert(false); 840 | } 841 | // How can we tell which ball pixel we're going to be? The ball hasn't 842 | // moved yet, so we can't just look at our neighbors. We can tell from bs 843 | // where the ball's coming from. It must have dX != 0, and dY can be in 844 | // [-1,0,1]. We should be able to see enough pixels to know--either we can 845 | // see ball pixels, or we can see above or below, and so detect an edge. 846 | // If dY === 0, check offsets (-dX, 1), and (-dX, 0) for isBall. 847 | // Hmm...check them all anyway. If you see the bottom or top edge, that 848 | // tells you where it is now. If you don't that tells you too. Then use dY 849 | // to tell you where it's going to be. 850 | let ballNextPos = ballCurPos + ballDY; 851 | let paddlePixel = 852 | ballNextPos + getPaddlePixelHelper(data[1], data[4], data[7]); 853 | if (paddlePixel >= 0 && paddlePixel <= 7) { 854 | return { value: paddlePixel }; 855 | } 856 | return null; 857 | } 858 | 859 | /* Assumes this encoding and a 10-pixel paddle region now: 0100011101. 860 | Assumes the center pixel is in the paddle region. 861 | Returns the paddle pixel value for that center pixel. 862 | Returns a value between -1 and 8. 863 | 864 | xxxb 865 | xxxb 866 | xxxp // This is the highest hit; above this it's a miss. 867 | p // So let's call that 0. Above that is -1. Since the center 868 | p // pixel in our data is guaranteed to be in-region, we can't get 869 | p // -2 or above. 870 | p 871 | p 872 | b 873 | b 874 | 875 | b 876 | b 877 | p 878 | p 879 | p 880 | p 881 | p 882 | xxxp // This is the lowest hit; below this it's a miss. 883 | xxxb // It's pixel 7. So there are 8 valid positions, 6 for the length 884 | xxxb // of the paddle, plus 2 for the excess width of the ball, using the 885 | // 10 slots of the single-bit encoding. 886 | */ 887 | 888 | function getPaddlePixelHelper(d0, d1, d2) { 889 | let isP0 = isInPaddleBufferRegion(d0); 890 | assert(isInPaddleBufferRegion(d1)); 891 | let isP2 = isInPaddleBufferRegion(d2); 892 | assert(isP0 || isP2); 893 | if (!isP0) { 894 | return -1; 895 | } else if (!isP2) { 896 | return 8; 897 | } 898 | let code = 0; 899 | for (let d of [d0, d1, d2]) { 900 | let bit; 901 | if (isBall(d)) { 902 | bit = nsBall.PADDLE_PIXEL.get(d); 903 | } else { 904 | bit = nsBackground.PADDLE_PIXEL.get(d); 905 | } 906 | code = ((code << 1) | bit) >>> 0; 907 | } 908 | switch (code) { 909 | case 2: 910 | return 0; 911 | case 4: 912 | return 1; 913 | case 0: 914 | return 2; 915 | case 1: 916 | return 3; 917 | case 3: 918 | return 4; 919 | case 7: 920 | return 5; 921 | case 6: 922 | return 6; 923 | case 5: 924 | return 7; 925 | default: 926 | assert(false); 927 | break; 928 | } 929 | } 930 | 931 | // This takes care of moving balls only, not stationary ones. 932 | function handleIncomingBall(data, x, y) { 933 | for (let i = 0; i < 9; ++i) { 934 | let color = data[i]; 935 | if (isBall(color)) { 936 | if (!isBallMotionCycle(color)) { 937 | break; 938 | } 939 | // With a diagonal entry to the buffer, a trailing ball pixel moving 940 | // into the buffer for the first time [so no depth count] can hit an 941 | // edge buffer pixel even if it's time to bounce. We need to check all 942 | // neighboring ball pixels and take the highest depth on the way in; 943 | // they'll all match on the way out. 944 | let bs = new BallState(nsBall, color); 945 | // TODO: Can we make this more robust now that we know which buffer it 946 | // is? 947 | if (!bs.getDepthX() && (nsBall.BUFFER_X_MIN_FLAG.isSet(color) || 948 | nsBall.BUFFER_X_MAX_FLAG.isSet(color))) { 949 | // The ball has hit the end wall and should vanish, so ignore it. 950 | break; 951 | } 952 | let source = sourceDirectionFromIndex(i); 953 | if (source.dX === bs.dX && source.dY === bs.dY) { 954 | const current = data[4]; 955 | let allMotions = _(data) 956 | .filter(d => isBall(d)) 957 | .map(b => new BallState(nsBall, b)) 958 | .value(); 959 | let maxDepthX = _(allMotions) 960 | .map(m => m.getDepthX()) 961 | .max(); 962 | let maxDepthY = _(allMotions) 963 | .map(m => m.getDepthY()) 964 | .max(); 965 | bs.setDepthX(maxDepthX); 966 | bs.setDepthY(maxDepthY); 967 | // It's a hit; lets see if it's also bouncing or in a buffer. 968 | let bufferBits = getBufferBits(data, bs); 969 | let bufferXMin = bufferBits.xMin; 970 | let bufferXMax = bufferBits.xMax; 971 | let bufferYMin = bufferBits.yMin; 972 | let bufferYMax = bufferBits.yMax; 973 | 974 | if (bs.dX > 0 && bufferXMax) { 975 | bs.incDepthX(); 976 | } else if (bs.dX < 0 && bufferXMin) { 977 | bs.incDepthX(); 978 | } else if (bs.getDepthX() && bs.dX > 0 && !bufferXMax) { 979 | bs.decDepthX(); 980 | } else if (bs.getDepthX() && bs.dX < 0 && !bufferXMin) { 981 | bs.decDepthX(); 982 | } 983 | if (bs.dY > 0 && bufferYMax) { 984 | bs.incDepthY(); 985 | } else if (bs.dY < 0 && bufferYMin) { 986 | bs.incDepthY(); 987 | } else if (bs.getDepthY() && bs.dY > 0 && !bufferYMax) { 988 | bs.decDepthY(); 989 | } else if (bs.getDepthY() && bs.dY < 0 && !bufferYMin) { 990 | bs.decDepthY(); 991 | } 992 | if (bs.getDepthX() >= BUFFER_SIZE) { 993 | assert(bs.getDepthX() <= BUFFER_SIZE); 994 | // Mark the ball for bounce or destruction. 995 | let worthChecking = 996 | isPaddleBuffer(current) || isBallInPaddleBuffer(current); 997 | let ballDataColumn, v; 998 | if (bs.right) { 999 | ballDataColumn = [0, 3, 6]; 1000 | } else { 1001 | ballDataColumn = [2, 5, 8]; 1002 | } 1003 | 1004 | if (worthChecking && 1005 | (v = getPaddlePixel(bs.dY, data, ballDataColumn, x, y))) { 1006 | // This bounce kicks us away from the wall immediately. 1007 | bs.bounce('x', v.value) 1008 | } else { 1009 | bs.setDepthX(0); 1010 | } 1011 | } 1012 | if (bs.getDepthY() >= BUFFER_SIZE) { 1013 | assert(bs.getDepthY() <= BUFFER_SIZE); 1014 | if ((bufferYMax && bs.down) || 1015 | (bufferYMin && !bs.down)) { 1016 | // Reflect doesn't do any extra kick away from the wall, so a ball 1017 | // can remain at full depth for several cycles. We only want to 1018 | // reflect if we're heading toward the wall. 1019 | bs.reflect('y') 1020 | } 1021 | } 1022 | let respawn; 1023 | let bufferYFlag = bufferYMin || bufferYMax; 1024 | let nextColor = bs.nextColor(); 1025 | nextColor = nsBall.DECIMATOR.set(nextColor, 0); 1026 | nextColor = nsBall.BUFFER_X_MIN_FLAG.set(nextColor, bufferXMin); 1027 | nextColor = nsBall.BUFFER_X_MAX_FLAG.set(nextColor, bufferXMax); 1028 | nextColor = nsBall.BUFFER_Y_FLAG.set(nextColor, bufferYFlag); 1029 | nextColor = nsBall.PADDLE_BUFFER_FLAG.set(nextColor, 1030 | bufferBits.paddleBuffer); 1031 | if (isBall(current)) { 1032 | respawn = nsBall.RESPAWN_FLAG.get(current); 1033 | if (nsBall.PADDLE_BUFFER_FLAG.isSet(current)) { 1034 | let paddleBits = nsBall.PADDLE_BALL_BITS.get(current); 1035 | nextColor = nsBall.PADDLE_BALL_BITS.set(nextColor, paddleBits); 1036 | } else { 1037 | nextColor = nsBall.PADDLE_BALL_BITS.setMask(nextColor, false); 1038 | } 1039 | } else { 1040 | assert(isBackground(current)); 1041 | respawn = isRespawn(current); 1042 | if (isPaddleBuffer(current)) { 1043 | nextColor = 1044 | BitManager.copyBits(nsBall, nextColor, nsBackground, current, 1045 | copySets.PADDLE_BALL_BITS) 1046 | } else { 1047 | nextColor = nsBall.PADDLE_BALL_BITS.setMask(nextColor, false); 1048 | } 1049 | } 1050 | nextColor = nsBall.RESPAWN_FLAG.set(nextColor, respawn); 1051 | return { value: nextColor }; 1052 | } 1053 | } 1054 | } 1055 | return null; 1056 | } 1057 | 1058 | function setAIMessage(color, messageRightNotL, yInPaddleCoords) { 1059 | color = nsBackground.MESSAGE_PRESENT.setMask(color, true); 1060 | color = nsBackground.MESSAGE_H_NOT_V.setMask(color, true); 1061 | color = nsBackground.MESSAGE_R_NOT_L.setMask(color, messageRightNotL); 1062 | return nsBackground.MESSAGE_PADDLE_POSITION.set(color, 1063 | yInPaddleCoords >>> 3); 1064 | } 1065 | 1066 | /* When we bounce, we make sure the ball has a state that moves off the paddle 1067 | immediately. We know the length of its cycle from the move table. But 1068 | it's quite fiddly to determine how many Y moves happen in precisely the X 1069 | distance across, due to quantization and the Bresenham algorithm. To 1070 | figure out exactly when the ball will strike the far end, we restrict the 1071 | width of the field to be 1 mod 6, since all supported slopes divide 6. We 1072 | use the width without that extra 1 as the distance for the slope 1073 | calculation, then add back in the known motion from the current ball state 1074 | for the last pixel. 1075 | */ 1076 | function getNewAIMessage(data, x, y, color) { 1077 | let current = data[4]; 1078 | let above = false; 1079 | let messageRightNotL = false; 1080 | if (isPaddleBuffer(current) && 1081 | ((messageRightNotL = (isTrough(data[3]) || isPaddle(data[3]))) || 1082 | (isTrough(data[5]) || isPaddle(data[5]))) && 1083 | ((above = isBall(data[1])) || isBall(data[7]))) { 1084 | assert(paddleToPaddleBallDistance % 6 === 1); 1085 | 1086 | let ball = above ? data[1] : data[7]; 1087 | if (!isBallMotionCycle(ball) && 1088 | (nsBall.BUFFER_X_DEPTH_COUNTER.get(ball) === BUFFER_SIZE)) { 1089 | let bs = new BallState(nsBall, ball); 1090 | let paddlePixel = getPaddlePixel(0, data, [1, 4, 7], x, y).value; 1091 | let start = nsBackground.PADDLE_POSITION.get(current) + paddlePixel; 1092 | 1093 | let dY = bs.getSlope() * (paddleToPaddleBallDistance - 1); 1094 | if (!bs.down) { 1095 | dY = -dY 1096 | } 1097 | dY += bs.dY; // add in the dY for the last pixel of x traveled 1098 | let fullY = start + dY; 1099 | let clippedY = fullY % topWallToBottomWallBallDistance; 1100 | if (clippedY < 0) { 1101 | clippedY += topWallToBottomWallBallDistance; 1102 | } 1103 | assert(clippedY >= 0 && clippedY < topWallToBottomWallBallDistance); 1104 | if (Math.floor(fullY / topWallToBottomWallBallDistance) % 2) { 1105 | clippedY = topWallToBottomWallBallDistance - clippedY 1106 | } 1107 | return { value: setAIMessage(color, messageRightNotL, clippedY) }; 1108 | } 1109 | } 1110 | return null; 1111 | } 1112 | 1113 | function getAIMessage(data, x, y, color) { 1114 | let current = data[4]; 1115 | // preexisting message 1116 | for (let i of [0, 2, 3, 5, 6, 8]) { 1117 | let source = data[i]; 1118 | let active = isBackground(source) && 1119 | nsBackground.MESSAGE_PRESENT.isSet(source) && 1120 | nsBackground.MESSAGE_H_NOT_V.isSet(source); 1121 | if (active && 1122 | (nsBackground.MESSAGE_R_NOT_L.isSet(source) === (i % 3 === 0))) { 1123 | let bits = nsBackground.ALL_MESSAGE_BITS.get(source); 1124 | return nsBackground.ALL_MESSAGE_BITS.set(color, bits); 1125 | } 1126 | } 1127 | let v; 1128 | if (v = getNewAIMessage(data, x, y, color)) { 1129 | return v.value; 1130 | } 1131 | return nsBackground.ALL_MESSAGE_BITS.setMask(color, false); 1132 | } 1133 | 1134 | function handleAIMessageInPaddleBuffer(data, x, y, nextColor) { 1135 | let current = data[4]; 1136 | let flag = isBall(current) ? nsBall.BUFFER_X_MIN_FLAG : 1137 | nsBackground.BUFFER_X_MIN_FLAG; 1138 | let isLeft = flag.isSet(current); 1139 | if ((isLeft && leftPlayerHuman) || (!isLeft && rightPlayerHuman)) { 1140 | return nextColor; 1141 | } 1142 | if (nsBackground.MESSAGE_PRESENT.isSet(nextColor)) { 1143 | let isLeadingEdge, isNotForUs = false; 1144 | // TODO: Can we now leverage the MIN/MAX flags to make this simpler? 1145 | if (isBall(data[3]) || isBall(data[5])) { 1146 | // No message for us comes while a ball is nearby. 1147 | isNotForUs = true; 1148 | } else if (isPaddle(data[3]) || isTrough(data[3])) { 1149 | // Left paddle, left edge 1150 | assert(isLeft) 1151 | isLeadingEdge = false; 1152 | } else if (isPaddle(data[5]) || isTrough(data[5])) { 1153 | // Right paddle, right edge 1154 | assert(!isLeft); 1155 | isLeadingEdge = false; 1156 | } else if (!isPaddleBuffer(data[3])) { 1157 | // Right paddle, left edge 1158 | assert(!isLeft); 1159 | isLeadingEdge = true; 1160 | } else if (!isPaddleBuffer(data[5])) { 1161 | // Left paddle, right edge 1162 | assert(isLeft) 1163 | isLeadingEdge = true; 1164 | } else if (nsBackground.PADDLE_MOVE_DELAY_COUNTER.get(data[3])) { 1165 | // Right paddle, middle 1166 | assert(!isLeft); 1167 | isLeadingEdge = false; 1168 | } else if (nsBackground.PADDLE_MOVE_DELAY_COUNTER.get(data[5])) { 1169 | // Left paddle, middle 1170 | assert(isLeft) 1171 | isLeadingEdge = false; 1172 | } else { 1173 | // This is a message going the wrong direction for us. 1174 | // Some of the above messages may not be for us, but we don't know in 1175 | // all cases, so we'll figure it out below. 1176 | isNotForUs = true; 1177 | } 1178 | // Can't get a message while we're moving. 1179 | assert(nsBackground.PADDLE_DEST.get(nextColor) === 1180 | (nsBackground.PADDLE_POSITION.get(nextColor) >>> 3)); 1181 | if (!isNotForUs && 1182 | (nsBackground.MESSAGE_R_NOT_L.isSet(nextColor) !== isLeft)) { 1183 | let dest = nsBackground.MESSAGE_PADDLE_POSITION.get(nextColor); 1184 | nextColor = nsBackground.PADDLE_DEST.set(nextColor, dest); 1185 | if (isLeadingEdge) { 1186 | nextColor = 1187 | nsBackground.PADDLE_MOVE_DELAY_COUNTER.set( 1188 | nextColor, BUFFER_SIZE); 1189 | } else { 1190 | let counter = 1191 | nsBackground.PADDLE_MOVE_DELAY_COUNTER.get( 1192 | data[isLeft ? 5 : 3]); 1193 | nextColor = 1194 | nsBackground.PADDLE_MOVE_DELAY_COUNTER.set(nextColor, 1195 | counter - 1); 1196 | } 1197 | } 1198 | } else { 1199 | let counter = nsBackground.PADDLE_MOVE_DELAY_COUNTER.get(nextColor); 1200 | if (counter > 0) { 1201 | nextColor = nsBackground.PADDLE_MOVE_DELAY_COUNTER.set(nextColor, 1202 | counter - 1); 1203 | } 1204 | } 1205 | return nextColor; 1206 | } 1207 | 1208 | function getBackgroundOrBallSourceInfo(data, x, y) { 1209 | const current = data[4]; 1210 | let respawn; 1211 | let bufferXMinFlag; 1212 | let bufferXMaxFlag; 1213 | let bufferYFlag; 1214 | let nextColor; 1215 | let decimator; 1216 | let willBeBall = false; 1217 | let nsOutput; 1218 | if (isBall(current)) { 1219 | decimator = nsBall.DECIMATOR.isSet(current); 1220 | respawn = nsBall.RESPAWN_FLAG.get(current); 1221 | bufferXMinFlag = nsBall.BUFFER_X_MIN_FLAG.get(current); 1222 | bufferXMaxFlag = nsBall.BUFFER_X_MAX_FLAG.get(current); 1223 | bufferYFlag = nsBall.BUFFER_Y_FLAG.get(current); 1224 | if (!isBallMotionCycle(current)) { // else it's becoming background. 1225 | if ((bufferXMinFlag || bufferXMaxFlag) && 1226 | !nsBall.BUFFER_X_DEPTH_COUNTER.get(current)) { 1227 | // The ball has hit the end wall and should vanish, so ignore it. 1228 | } else { 1229 | willBeBall = true; 1230 | let bs = new BallState(nsBall, current); 1231 | nsOutput = nsBall; 1232 | // This already has respawn, bufferXMin/MaxFlag and bufferYFlag set 1233 | // correctly. 1234 | nextColor = bs.nextColor(); 1235 | nextColor = nsBall.PADDLE_BALL_BITS.set(nextColor, 0); 1236 | } 1237 | } 1238 | } else { 1239 | assert(isBackground(current)); 1240 | respawn = isRespawn(current); 1241 | bufferXMinFlag = nsBackground.BUFFER_X_MIN_FLAG.get(current); 1242 | bufferXMaxFlag = nsBackground.BUFFER_X_MAX_FLAG.get(current); 1243 | bufferYFlag = nsBackground.BUFFER_Y_FLAG.get(current); 1244 | if (respawn || isPaddleBuffer(current)) { 1245 | decimator = nsBackground.DECIMATOR.isSet(current); 1246 | } 1247 | } 1248 | if (!willBeBall) { 1249 | nsOutput = nsBackground; 1250 | nextColor = nsBackground.BASIC_BACKGROUND.getMask() 1251 | nextColor = nsOutput.BUFFER_X_MIN_FLAG.set(nextColor, bufferXMinFlag); 1252 | nextColor = nsOutput.BUFFER_X_MAX_FLAG.set(nextColor, bufferXMaxFlag); 1253 | nextColor = nsOutput.BUFFER_Y_FLAG.set(nextColor, bufferYFlag); 1254 | nextColor = nsOutput.RESPAWN_FLAG.set(nextColor, respawn); 1255 | nextColor = getAIMessage(data, x, y, nextColor); 1256 | } 1257 | let info = { 1258 | respawn: respawn, 1259 | bufferXMinFlag: bufferXMinFlag, 1260 | bufferXMaxFlag: bufferXMaxFlag, 1261 | bufferYFlag: bufferYFlag, 1262 | nextColor: nextColor, 1263 | decimator: decimator, 1264 | willBeBall : willBeBall, 1265 | nsOutput: nsOutput 1266 | }; 1267 | return info; 1268 | } 1269 | 1270 | function willItBeAPaddleBuffer(data, x, y, info) { 1271 | const current = data[4]; 1272 | let willBePaddleBuffer = false; 1273 | if (isInPaddleBufferRegion(current) && 1274 | !isPaddleMotionCycleGeneral(current)) { 1275 | info.paddleBufferBitsSource = current; 1276 | willBePaddleBuffer = true; 1277 | info.nsSource = isBall(current) ? nsBall : nsBackground; 1278 | } else { 1279 | for (let i of [4, 1, 7]) { 1280 | let color = data[i]; 1281 | if (isInPaddleBufferRegion(color)) { 1282 | if (i !== 4 && !isPaddleMotionCycleGeneral(color)) { 1283 | break; // early-out depends on trying 4 first 1284 | } 1285 | let ps = new PaddleState(color); 1286 | let source = sourceDirectionFromIndex(i); 1287 | let flag = isBall(color) ? nsBall.BUFFER_X_MIN_FLAG : 1288 | nsBackground.BUFFER_X_MIN_FLAG; 1289 | let isLeft = flag.isSet(color); 1290 | if (source.dY === ps.getDY(isLeft)) { 1291 | willBePaddleBuffer = true; 1292 | info.paddleBufferBitsSource = ps.nextColor(isLeft); 1293 | info.decimator = ps.decimator; 1294 | info.nsSource = ps.getNamespace(); 1295 | break; 1296 | } 1297 | } 1298 | } 1299 | } 1300 | return willBePaddleBuffer; 1301 | } 1302 | 1303 | function handleBecomingOrStayingBackgroundOrStayingBall(data, x, y) { 1304 | const current = data[4]; 1305 | let info = getBackgroundOrBallSourceInfo(data, x, y); 1306 | 1307 | let nextColor = info.nextColor; 1308 | let willBeBall = info.willBeBall; 1309 | let nsOutput = info.nsOutput; 1310 | 1311 | let willBePaddleBuffer = false; 1312 | if (info.bufferXMinFlag || info.bufferXMaxFlag || info.bufferYFlag) { 1313 | assert(!info.respawn); 1314 | if (info.bufferXMinFlag | info.bufferXMaxFlag) { 1315 | willBePaddleBuffer = willItBeAPaddleBuffer(data, x, y, info); 1316 | if (willBePaddleBuffer) { 1317 | nextColor = nsOutput.PADDLE_BUFFER_FLAG.set(nextColor, true); 1318 | if (willBeBall === isBall(info.paddleBufferBitsSource)) { 1319 | assert(nsOutput === info.nsSource); 1320 | let bitFlag; 1321 | if (willBeBall) { 1322 | bitFlag = nsBall.PADDLE_BALL_BITS; 1323 | } else { 1324 | bitFlag = nsBackground.PADDLE_BACKGROUND_BITS; 1325 | } 1326 | nextColor = bitFlag.set(nextColor, 1327 | bitFlag.get(info.paddleBufferBitsSource)); 1328 | } else { 1329 | nextColor = 1330 | BitManager.copyBits(nsOutput, nextColor, info.nsSource, 1331 | info.paddleBufferBitsSource, 1332 | copySets.PADDLE_BALL_BITS) 1333 | } 1334 | // If an AI message is passing through, process it. 1335 | if (!willBeBall) { 1336 | nextColor = handleAIMessageInPaddleBuffer(data, x, y, nextColor); 1337 | } 1338 | } 1339 | } 1340 | } 1341 | if (!willBeBall && !willBePaddleBuffer) { 1342 | if (isRespawn(data[3]) && isRespawn(data[7]) && 1343 | nsBackground.MESSAGE_PRESENT.isSet(data[3]) && 1344 | !nsBackground.MESSAGE_H_NOT_V.isSet(data[3]) && 1345 | nsBackground.MESSAGE_R_NOT_L.isSet(data[3])) { 1346 | // Depends on RESPAWN_DOWN and RESPAWN_INDEX 1347 | nextColor = setAIMessage(nextColor, true, RESPAWN_PADDLE_DEST); 1348 | } else if (isRespawn(data[5]) && isRespawn(data[7]) && 1349 | nsBackground.MESSAGE_PRESENT.isSet(data[5]) && 1350 | !nsBackground.MESSAGE_H_NOT_V.isSet(data[5]) && 1351 | !nsBackground.MESSAGE_R_NOT_L.isSet(data[5])) { 1352 | // Depends on RESPAWN_DOWN and RESPAWN_INDEX 1353 | nextColor = setAIMessage(nextColor, false, RESPAWN_PADDLE_DEST); 1354 | } 1355 | } 1356 | if (willBeBall || willBePaddleBuffer || info.respawn) { 1357 | assert(info.decimator !== undefined); 1358 | nextColor = nsOutput.DECIMATOR.setMask(nextColor, !info.decimator); 1359 | } 1360 | return nextColor; 1361 | } 1362 | 1363 | function pong(data, x, y) { 1364 | const current = data[4]; 1365 | let v; 1366 | 1367 | if (isScoreboard(current)) { 1368 | return handleScoreboard(data, x, y); 1369 | } 1370 | 1371 | if (isWall(current)) { 1372 | return handleWall(data, x, y); 1373 | } 1374 | 1375 | // Trough doesn't have to deal with balls, messages, or respawn, only ball 1376 | // deaths and eventually the paddle. 1377 | if (isTrough(current) || isPaddle(current)) { 1378 | return handleTroughAndPaddle(data, x, y); 1379 | } 1380 | 1381 | // We won't receive a respawn message and a ball in the same cycle. 1382 | if (v = handleRespawnMessage(data, x, y)) { 1383 | return v.value; 1384 | } 1385 | 1386 | // Moving balls are handled here, stationary balls and background will be 1387 | // dealt with in handleBecomingOrStayingBackgroundOrStayingBall. 1388 | if (v = handleIncomingBall(data, x, y)) { 1389 | return v.value; 1390 | } 1391 | 1392 | return handleBecomingOrStayingBackgroundOrStayingBall(data, x, y); 1393 | } 1394 | 1395 | let width = DESIRED_BALL_AREA_WIDTH + 4; // 2x trough, 2x wall 1396 | let height = DESIRED_BALL_AREA_HEIGHT + 2 + SCOREBOARD_HEIGHT; // 2x wall 1397 | registerAnimation("pong", width, height, initPong, pong); 1398 | 1399 | })(); 1400 | -------------------------------------------------------------------------------- /scoreboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | (function () { 4 | let nsScoreboard, isScoreboard; 5 | let scoreboardColor, fullAlpha; 6 | let isSendingMessageDown, isSignallingGameOver; 7 | function initScoreboard(_nsScoreboard_, _scoreboardColor_, _fullAlpha_, 8 | _isScoreboard_, _isSendingMessageDown_, _isSignallingGameOver_, 9 | obviousColors) { 10 | nsScoreboard = _nsScoreboard_; 11 | scoreboardColor = _scoreboardColor_; 12 | fullAlpha = _fullAlpha_; // the on alpha mask 13 | isScoreboard = _isScoreboard_; 14 | isSendingMessageDown = _isSendingMessageDown_; 15 | isSignallingGameOver = _isSignallingGameOver_; 16 | nsScoreboard.declare('SCOREBOARD_COLOR', 2, 22); 17 | if (obviousColors) { 18 | nsScoreboard.declare('SCOREBOARD_CHANGED', 3, 4); 19 | nsScoreboard.declare('SCOREBOARD_SEGMENT_ID', 3, 10); 20 | } else { 21 | nsScoreboard.declare('SCOREBOARD_SEGMENT_ID', 3, 0); 22 | nsScoreboard.declare('SCOREBOARD_CHANGED', 1, 25); 23 | } 24 | nsScoreboard.alloc('SCOREBOARD_BITS', 5); 25 | nsScoreboard.declare('SCOREBOARD_HIGH_DIGIT', 1, 24); 26 | } 27 | window.initScoreboard = initScoreboard; 28 | 29 | function drawDigit(c, digitBit, x, y, l, w) { 30 | let value = 0; 31 | let onMask = bm.or([nsScoreboard.SCOREBOARD_COLOR.getMask(), 32 | fullAlpha.getMask()]); 33 | let baseColor = 34 | nsScoreboard.SCOREBOARD_HIGH_DIGIT.set(scoreboardColor, digitBit); 35 | baseColor = nsScoreboard.SCOREBOARD_BITS.set(baseColor, value); 36 | 37 | let color, on; 38 | 39 | on = isSegmentOn(digitBit, 1, value); 40 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 41 | color = fullAlpha.setMask(color, on); 42 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 1), 43 | x + w, 44 | y, 45 | l, w); 46 | on = isSegmentOn(digitBit, 2, value); 47 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 48 | color = fullAlpha.setMask(color, on); 49 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 2), 50 | x + l + w, 51 | y + w, 52 | w, l); 53 | on = isSegmentOn(digitBit, 3, value); 54 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 55 | color = fullAlpha.setMask(color, on); 56 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 3), 57 | x + l + w, 58 | y + 2 * w + l, 59 | w, l); 60 | on = isSegmentOn(digitBit, 4, value); 61 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 62 | color = fullAlpha.setMask(color, on); 63 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 4), 64 | x + w, 65 | y + 2 * w + 2 * l, 66 | l, w); 67 | on = isSegmentOn(digitBit, 5, value); 68 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 69 | color = fullAlpha.setMask(color, on); 70 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 5), 71 | x, 72 | y + 2 * w + l, 73 | w, l); 74 | on = isSegmentOn(digitBit, 6, value); 75 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 76 | color = fullAlpha.setMask(color, on); 77 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 6), 78 | x, 79 | y + w, 80 | w, l); 81 | on = isSegmentOn(digitBit, 7, value); 82 | color = nsScoreboard.SCOREBOARD_COLOR.setMask(baseColor, on); 83 | color = fullAlpha.setMask(color, on); 84 | c.orRect(nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 7), 85 | x + w, 86 | y + w + l, 87 | l, w); 88 | } 89 | 90 | function drawScoreboard(c, left, top, width, height) { 91 | c.fillRect(scoreboardColor, left, top, width, height); 92 | const SEGMENT_LENGTH = 93 | Math.floor(Math.min((width - 7) / 2, 94 | (height - 5) / 2)); 95 | const SEGMENT_THICKNESS = 1; 96 | drawDigit(c, 1, left + 1, top + 1, SEGMENT_LENGTH, SEGMENT_THICKNESS); 97 | drawDigit(c, 0, left + width - 3 - SEGMENT_LENGTH, top + 1, 98 | SEGMENT_LENGTH, SEGMENT_THICKNESS); 99 | } 100 | window.drawScoreboard = drawScoreboard; 101 | 102 | // Use the middle segment to write the words, cheating value so that the 103 | // increment turns it on. Note that there can be display issues if 104 | // GAME_OVER_SCORE is near the value used. 105 | const SCOREBOARD_GAME_OVER_PREP_VALUE = 27; 106 | function drawGameOver(c, left, top, width, height) { 107 | const MESSAGE = [ 108 | " XX XX XX XX XXXX", 109 | "X X X X X X X X ", 110 | "X XX XXXX X X X XXX ", 111 | "X X X X X X X ", 112 | " XX X X X X XXXX", 113 | " ", 114 | " XX X X XXXX XXX ", 115 | " X X X X X X X ", 116 | " X X X X XXX XXX ", 117 | " X X X X X X X ", 118 | " XX X XXXX X X "]; 119 | 120 | let color = 121 | nsScoreboard.SCOREBOARD_BITS.set(scoreboardColor, 122 | SCOREBOARD_GAME_OVER_PREP_VALUE); 123 | let fgColor = nsScoreboard.SCOREBOARD_SEGMENT_ID.set(color, 7); 124 | fgColor = nsScoreboard.SCOREBOARD_HIGH_DIGIT.set(fgColor, 1); 125 | let key = { 126 | ' ' : color, 127 | 'X': fgColor 128 | }; 129 | let messageWidth = MESSAGE[0].length; 130 | c.fillRect(color, left, top, width, height); 131 | left += Math.floor((width - messageWidth) / 2); 132 | c.fillBitmap(left, top, MESSAGE, key); 133 | } 134 | window.drawGameOver = drawGameOver; 135 | 136 | // Segments numbered clockwise from top, then the middle last. 137 | // This table tells whether the segment is on for a given digit. 138 | const SEGMENT_TABLE = [ 139 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 140 | [1, 0, 1, 1, 0, 1, 1, 1, 1, 1], 141 | [1, 1, 1, 1, 1, 0, 0, 1, 1, 1], 142 | [1, 1, 0, 1, 1, 1, 1, 1, 1, 1], 143 | [1, 0, 1, 1, 0, 1, 1, 0, 1, 0], 144 | [1, 0, 1, 0, 0, 0, 1, 0, 1, 0], 145 | [1, 0, 0, 0, 1, 1, 1, 0, 1, 1], 146 | [0, 0, 1, 1, 1, 1, 1, 0, 1, 1] 147 | ] 148 | 149 | function isSegmentOn(highDigit, segment, value) { 150 | let digit; 151 | if (highDigit) { 152 | digit = Math.floor((value + 0.5) / 10) % 10; 153 | if (!digit) { // high digit never shows a zero 154 | return false; 155 | } 156 | } else { 157 | digit = value % 10; 158 | } 159 | return SEGMENT_TABLE[segment][digit] === 1; 160 | } 161 | 162 | function handleScoreboard(data, x, y) { 163 | let current = data[4]; 164 | assert(isScoreboard(current)); 165 | let curValue = nsScoreboard.SCOREBOARD_BITS.get(current); 166 | let value = _(data) 167 | .map(c => isScoreboard(c) ? nsScoreboard.SCOREBOARD_BITS.get(c) : 0) 168 | .max() 169 | let changed = curValue !== value; 170 | if (!changed && (isSendingMessageDown(data[1]) || 171 | ((curValue === SCOREBOARD_GAME_OVER_PREP_VALUE) && 172 | (isSignallingGameOver(data[3]) || 173 | isSignallingGameOver(data[5]))))) { 174 | ++value; 175 | changed = true; 176 | } 177 | if (changed) { 178 | let segment = nsScoreboard.SCOREBOARD_SEGMENT_ID.get(current); 179 | let highDigit = nsScoreboard.SCOREBOARD_HIGH_DIGIT.isSet(current); 180 | let on = isSegmentOn(highDigit, segment, value); 181 | let next = nsScoreboard.SCOREBOARD_BITS.set(current, value); 182 | next = nsScoreboard.SCOREBOARD_COLOR.setMask(next, on); 183 | next = fullAlpha.setMask(next, true); 184 | return nsScoreboard.SCOREBOARD_CHANGED.setMask(next, true); 185 | } 186 | if (!nsScoreboard.SCOREBOARD_COLOR.isSet(current)) { 187 | let next = nsScoreboard.SCOREBOARD_CHANGED.setMask(current, false); 188 | return fullAlpha.setMask(next, false); 189 | } else { 190 | return nsScoreboard.SCOREBOARD_CHANGED.setMask(current, false); 191 | } 192 | return current; 193 | } 194 | window.handleScoreboard = handleScoreboard; 195 | 196 | /* 197 | XX X X X XXX X X X XXX XX 198 | X X X XX XX X X X X X X X X 199 | X XX XXX X X X XXX X X X X XXX XX 200 | X X X X X X X X X X X X X X 201 | XX X X X X XXX X X XXX X X 202 | */ 203 | 204 | })(); 205 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function assert(val, message) { 4 | if (!val) { 5 | var m = "Assertion failed!" 6 | if (message) { 7 | m += "\n" + message; 8 | } 9 | throw m; 10 | } 11 | } 12 | 13 | --------------------------------------------------------------------------------