├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.js ├── bg.jpg ├── build.js ├── build.js.map ├── config.js ├── css ├── interface.css └── tiles.css ├── data ├── levels_candidate.json ├── levels_concepts.json ├── levels_game.json ├── levels_last.json └── levels_other.json ├── deploy_dev.sh ├── deploy_play.sh ├── favicon.ico ├── index.html ├── index_alt.html ├── js ├── bare_board.js ├── config.js ├── const.js ├── detection_bar.js ├── drag_and_drop.js ├── game.js ├── game_board.js ├── level.js ├── level.spec.js ├── level_io_uri.js ├── level_io_uri.spec.js ├── logger.js ├── particle │ ├── canvas_particle_animation.js │ ├── particle.js │ ├── particle_animation.js │ ├── particle_animation.spec.js │ └── svg_particle_animation.js ├── popup_manager.js ├── print.js ├── progress_pearls.js ├── simulation.js ├── sound_service.js ├── stock.js ├── storage.js ├── tensor │ ├── direction.js │ ├── full.js │ ├── full.spec.js │ ├── polarization.js │ ├── tensor.js │ └── tensor.spec.js ├── test_utils │ └── mock_d3.js ├── tile.js ├── tile_helper.js ├── title_manager.js ├── tooltip.js ├── transition_heatmap.js ├── views │ ├── encyclopedia_item_view.js │ ├── encyclopedia_selector_view.js │ ├── game_view.js │ ├── level_selector_view.js │ └── view.js ├── winning_status.js └── winning_status.spec.js ├── karma.conf.js ├── logo.svg ├── package-lock.json ├── package.json ├── screenshot_qg_dev.png └── sounds ├── absorber.mp3 ├── blip.mp3 ├── detector.mp3 ├── error.mp3 ├── mine.mp3 └── rock.mp3 /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "plugins": ["lodash"], 7 | "extends": ["eslint:recommended", "plugin:lodash/recommended"], 8 | "rules": { 9 | "comma-dangle": [1, "always-multiline"], 10 | "no-var": 2, 11 | "quotes": [1, "single"], 12 | "no-unused-vars": 1, 13 | "lodash/prefer-lodash-method": 0 14 | }, 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "jasmine": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [stared] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /aux 3 | /.idea 4 | /jspm_packages 5 | /node_modules 6 | /bundled/build.* 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.3-alpine 2 | 3 | # If Github throtlling is an issue try building with something like: 4 | # docker build --build-arg JSPM_GITHUB_AUTH_TOKEN="a_jspm_encrypted_github_token" . 5 | 6 | ARG JSPM_GITHUB_AUTH_TOKEN="" 7 | RUN mkdir /app 8 | WORKDIR /app 9 | ADD . /app 10 | RUN apk add --no-cache git && \ 11 | npm install --global karma-cli && \ 12 | npm install jspm -g && \ 13 | jspm config registries.github.auth ${JSPM_GITHUB_AUTH_TOKEN} && \ 14 | npm install http-server -g && \ 15 | npm install && \ 16 | jspm install -y && \ 17 | jspm bundle-sfx --minify app && \ 18 | jspm config registries.github.auth "" 19 | CMD ["http-server",".","-p","8080"] 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Piotr Migdał, Patryk Hes 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quantum Game with Photons (2014-2016) 2 | ============ 3 | 4 | **A note: this repository is an old Quanutm Game (2014-2016), no longer maintained. The new one (2019-) is at [quantumgame.io](http://quantumgame.io).** 5 | 6 | Quantum Game - play with photons, superposition and entanglement. In your browser! With true quantum mechanics underneath! 7 | 8 | * Official address: http://quantumgame.io (initial beta released on 3 Jun 2016, http://play.quantumgame.io) 9 | * Social media: Facebook: [Quantum Game with Photons](https://www.facebook.com/quantumgameio), Twitter: [@quantumgameio](https://twitter.com/quantumgameio) 10 | * Authors: [Piotr Migdał](http://p.migdal.pl), [Patryk Hes](https://github.com/pathes), [Michał Krupiński](http://www.fiztaszki.pl/user/3). 11 | * Supported by: [eNgage III/2014](http://www.fnp.org.pl/laureaci-engage-iii-edycja/) grant. 12 | * A recent screenshot: 13 | 14 | ![Screenshot](screenshot_qg_dev.png) 15 | 16 | 17 | # Development version 18 | 19 | It's JavaScript, ES6. To build it you need [Node.JS](https://nodejs.org/) and [jspm.io](http://jspm.io/) package manager. 20 | 21 | It's open for collaboration - from level creation, through interface (re)design and adding additional effects (two-photon interference, interactions with an electron). Interested? Mail pmigdal@gmail.com. 22 | 23 | 24 | ## Installing 25 | 26 | After installing Node.js and jspm.io, and cloning this repository: 27 | 28 | Then install local packages. 29 | ```bash 30 | npm install 31 | jspm install 32 | ``` 33 | 34 | Additionally, for development we use `eslint` with `eslint-plugin-lodash`. 35 | 36 | **A note: jspm is seriously outdated and the build may not work.** 37 | 38 | ## Running server 39 | 40 | Start local HTTP server in the quantum game directory (e.g. by [http-server](https://www.npmjs.com/package/http-server)). 41 | **Does not need an install, as there are pre-built files.** 42 | 43 | ## Running tests 44 | 45 | ```bash 46 | ./node_modules/.bin/karma start 47 | ``` 48 | 49 | # Production version 50 | 51 | Bundle it (and minify, if you want): 52 | 53 | ```bash 54 | jspm bundle-sfx --minify app 55 | ``` 56 | 57 | It creates a `build.js` file. To run it we need a modified `index.html` (it is a *manually*-modified file, stored in `bundled/index.html`). 58 | 59 | On the server, the structure of files should look as follows: 60 | 61 | ```bash 62 | css\ 63 | favicon.ico 64 | build.js 65 | index.html 66 | ``` 67 | 68 | ## Docker 69 | 70 | Alternatively, you can install dependencies using Docker. 71 | 72 | ### Building 73 | 74 | * You can build this image by running the following command in the root of this repository: `docker build .` 75 | * You can also pass in a valid JSPM_GITHUB_AUTH_TOKEN by building like this: `docker build --build-arg JSPM_GITHUB_AUTH_TOKEN="a_jspm_encrypted_github_token" .` 76 | * For more information see: https://stackoverflow.com/questions/30995040/jspm-saying-github-rate-limit-reached-how-to-fix 77 | 78 | ### Running 79 | 80 | * If your build completes sucessfully there will be a new image ID printed at the end of the build, which you can then use to to run it: `docker run -d -p 80:8080 ${IMAGE_ID_FROM_BUILD}` 81 | * or for a community built image try this: `docker run -d -p 80:8080 spkane/quantum-game:latest` 82 | 83 | and then open up a web browser and point it to port 80 on your Docker host. 84 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import 'normalize.css'; 3 | 4 | import * as game from './js/game'; 5 | 6 | const quantumGame = new game.Game(); 7 | quantumGame.htmlReady(); 8 | -------------------------------------------------------------------------------- /bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/bg.jpg -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | defaultJSExtensions: true, 3 | transpiler: "babel", 4 | babelOptions: { 5 | "optional": [ 6 | "runtime" 7 | ] 8 | }, 9 | paths: { 10 | "github:*": "jspm_packages/github/*", 11 | "npm:*": "jspm_packages/npm/*" 12 | }, 13 | 14 | map: { 15 | "babel": "npm:babel-core@5.8.38", 16 | "babel-runtime": "npm:babel-runtime@5.8.38", 17 | "clean-css": "npm:clean-css@3.4.27", 18 | "core-js": "npm:core-js@1.2.7", 19 | "d3": "github:d3/d3@3.5.17", 20 | "file-saver": "npm:file-saver@1.3.3", 21 | "json": "github:systemjs/plugin-json@0.1.2", 22 | "json-stringify-pretty-compact": "npm:json-stringify-pretty-compact@1.0.4", 23 | "lodash": "npm:lodash@4.17.4", 24 | "normalize.css": "github:necolas/normalize.css@3.0.3", 25 | "soundjs": "github:CreateJS/SoundJS@0.6.2", 26 | "github:jspm/nodelibs-assert@0.1.0": { 27 | "assert": "npm:assert@1.4.1" 28 | }, 29 | "github:jspm/nodelibs-buffer@0.1.1": { 30 | "buffer": "npm:buffer@5.0.6" 31 | }, 32 | "github:jspm/nodelibs-events@0.1.1": { 33 | "events": "npm:events@1.0.2" 34 | }, 35 | "github:jspm/nodelibs-http@1.7.1": { 36 | "Base64": "npm:Base64@0.2.1", 37 | "events": "github:jspm/nodelibs-events@0.1.1", 38 | "inherits": "npm:inherits@2.0.1", 39 | "stream": "github:jspm/nodelibs-stream@0.1.0", 40 | "url": "github:jspm/nodelibs-url@0.1.0", 41 | "util": "github:jspm/nodelibs-util@0.1.0" 42 | }, 43 | "github:jspm/nodelibs-https@0.1.0": { 44 | "https-browserify": "npm:https-browserify@0.0.0" 45 | }, 46 | "github:jspm/nodelibs-os@0.1.0": { 47 | "os-browserify": "npm:os-browserify@0.1.2" 48 | }, 49 | "github:jspm/nodelibs-path@0.1.0": { 50 | "path-browserify": "npm:path-browserify@0.0.0" 51 | }, 52 | "github:jspm/nodelibs-process@0.1.2": { 53 | "process": "npm:process@0.11.10" 54 | }, 55 | "github:jspm/nodelibs-stream@0.1.0": { 56 | "stream-browserify": "npm:stream-browserify@1.0.0" 57 | }, 58 | "github:jspm/nodelibs-url@0.1.0": { 59 | "url": "npm:url@0.10.3" 60 | }, 61 | "github:jspm/nodelibs-util@0.1.0": { 62 | "util": "npm:util@0.10.3" 63 | }, 64 | "github:jspm/nodelibs-vm@0.1.0": { 65 | "vm-browserify": "npm:vm-browserify@0.0.4" 66 | }, 67 | "github:necolas/normalize.css@3.0.3": { 68 | "css": "github:systemjs/plugin-css@0.1.35" 69 | }, 70 | "npm:amdefine@1.0.1": { 71 | "fs": "github:jspm/nodelibs-fs@0.1.2", 72 | "module": "github:jspm/nodelibs-module@0.1.0", 73 | "path": "github:jspm/nodelibs-path@0.1.0", 74 | "process": "github:jspm/nodelibs-process@0.1.2" 75 | }, 76 | "npm:assert@1.4.1": { 77 | "assert": "github:jspm/nodelibs-assert@0.1.0", 78 | "buffer": "github:jspm/nodelibs-buffer@0.1.1", 79 | "process": "github:jspm/nodelibs-process@0.1.2", 80 | "util": "npm:util@0.10.3" 81 | }, 82 | "npm:babel-runtime@5.8.38": { 83 | "process": "github:jspm/nodelibs-process@0.1.2" 84 | }, 85 | "npm:buffer@5.0.6": { 86 | "base64-js": "npm:base64-js@1.2.0", 87 | "ieee754": "npm:ieee754@1.1.8" 88 | }, 89 | "npm:clean-css@3.4.27": { 90 | "buffer": "github:jspm/nodelibs-buffer@0.1.1", 91 | "commander": "npm:commander@2.8.1", 92 | "fs": "github:jspm/nodelibs-fs@0.1.2", 93 | "http": "github:jspm/nodelibs-http@1.7.1", 94 | "https": "github:jspm/nodelibs-https@0.1.0", 95 | "os": "github:jspm/nodelibs-os@0.1.0", 96 | "path": "github:jspm/nodelibs-path@0.1.0", 97 | "process": "github:jspm/nodelibs-process@0.1.2", 98 | "source-map": "npm:source-map@0.4.4", 99 | "url": "github:jspm/nodelibs-url@0.1.0", 100 | "util": "github:jspm/nodelibs-util@0.1.0" 101 | }, 102 | "npm:commander@2.8.1": { 103 | "child_process": "github:jspm/nodelibs-child_process@0.1.0", 104 | "events": "github:jspm/nodelibs-events@0.1.1", 105 | "fs": "github:jspm/nodelibs-fs@0.1.2", 106 | "graceful-readlink": "npm:graceful-readlink@1.0.1", 107 | "path": "github:jspm/nodelibs-path@0.1.0", 108 | "process": "github:jspm/nodelibs-process@0.1.2" 109 | }, 110 | "npm:core-js@1.2.7": { 111 | "fs": "github:jspm/nodelibs-fs@0.1.2", 112 | "path": "github:jspm/nodelibs-path@0.1.0", 113 | "process": "github:jspm/nodelibs-process@0.1.2", 114 | "systemjs-json": "github:systemjs/plugin-json@0.1.2" 115 | }, 116 | "npm:core-util-is@1.0.2": { 117 | "buffer": "github:jspm/nodelibs-buffer@0.1.1" 118 | }, 119 | "npm:graceful-readlink@1.0.1": { 120 | "fs": "github:jspm/nodelibs-fs@0.1.2" 121 | }, 122 | "npm:https-browserify@0.0.0": { 123 | "http": "github:jspm/nodelibs-http@1.7.1" 124 | }, 125 | "npm:inherits@2.0.1": { 126 | "util": "github:jspm/nodelibs-util@0.1.0" 127 | }, 128 | "npm:os-browserify@0.1.2": { 129 | "os": "github:jspm/nodelibs-os@0.1.0" 130 | }, 131 | "npm:path-browserify@0.0.0": { 132 | "process": "github:jspm/nodelibs-process@0.1.2" 133 | }, 134 | "npm:process@0.11.10": { 135 | "assert": "github:jspm/nodelibs-assert@0.1.0", 136 | "fs": "github:jspm/nodelibs-fs@0.1.2", 137 | "vm": "github:jspm/nodelibs-vm@0.1.0" 138 | }, 139 | "npm:punycode@1.3.2": { 140 | "process": "github:jspm/nodelibs-process@0.1.2" 141 | }, 142 | "npm:readable-stream@1.1.14": { 143 | "buffer": "github:jspm/nodelibs-buffer@0.1.1", 144 | "core-util-is": "npm:core-util-is@1.0.2", 145 | "events": "github:jspm/nodelibs-events@0.1.1", 146 | "inherits": "npm:inherits@2.0.1", 147 | "isarray": "npm:isarray@0.0.1", 148 | "process": "github:jspm/nodelibs-process@0.1.2", 149 | "stream-browserify": "npm:stream-browserify@1.0.0", 150 | "string_decoder": "npm:string_decoder@0.10.31" 151 | }, 152 | "npm:source-map@0.4.4": { 153 | "amdefine": "npm:amdefine@1.0.1", 154 | "process": "github:jspm/nodelibs-process@0.1.2" 155 | }, 156 | "npm:stream-browserify@1.0.0": { 157 | "events": "github:jspm/nodelibs-events@0.1.1", 158 | "inherits": "npm:inherits@2.0.1", 159 | "readable-stream": "npm:readable-stream@1.1.14" 160 | }, 161 | "npm:string_decoder@0.10.31": { 162 | "buffer": "github:jspm/nodelibs-buffer@0.1.1" 163 | }, 164 | "npm:url@0.10.3": { 165 | "assert": "github:jspm/nodelibs-assert@0.1.0", 166 | "punycode": "npm:punycode@1.3.2", 167 | "querystring": "npm:querystring@0.2.0", 168 | "util": "github:jspm/nodelibs-util@0.1.0" 169 | }, 170 | "npm:util@0.10.3": { 171 | "inherits": "npm:inherits@2.0.1", 172 | "process": "github:jspm/nodelibs-process@0.1.2" 173 | }, 174 | "npm:vm-browserify@0.0.4": { 175 | "indexof": "npm:indexof@0.0.1" 176 | } 177 | } 178 | }); 179 | -------------------------------------------------------------------------------- /css/interface.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | padding: 0; 5 | margin: 0; 6 | font-size: 0; 7 | overflow: hidden; 8 | box-sizing: border-box; 9 | font-family: "Roboto"; 10 | } 11 | body { 12 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#4f569f+0,1a2f55+100 */ 13 | background: #4f569f; /* Old browsers */ 14 | background: -moz-radial-gradient(center, ellipse cover, #4f569f 0%, #1a2f55 100%); /* FF3.6-15 */ 15 | background: -webkit-radial-gradient(center, ellipse cover, #4f569f 0%,#1a2f55 100%); /* Chrome10-25,Safari5.1-6 */ 16 | background: radial-gradient(ellipse at center, #4f569f 0%,#1a2f55 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 17 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4f569f', endColorstr='#1a2f55',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 18 | background-image: url('../bg.jpg'); 19 | background-size: cover; 20 | } 21 | 22 | .svg-defs { 23 | /* Can't do display: none; http://stackoverflow.com/questions/25799770 */ 24 | display: block; 25 | position: absolute; 26 | top: 0; 27 | left: 0; 28 | width: 0; 29 | height: 0; 30 | } 31 | 32 | .top-bar { 33 | height: 10vh; 34 | color: white; 35 | position: relative; 36 | border-bottom: 2px solid white; 37 | background: rgba(255, 255, 255, 0.2); 38 | } 39 | 40 | .top-bar__title { 41 | position: absolute; 42 | top: 0; 43 | left: 15vw; 44 | width: 70vw; 45 | text-align: center; 46 | font-size: 7.5vh; 47 | vertical-align: middle; 48 | color: white; 49 | overflow: hidden; 50 | } 51 | 52 | .message-success { 53 | color: lime !important; 54 | fill: lime !important; 55 | } 56 | 57 | .message-failure { 58 | color: orange !important; 59 | fill: orange !important; 60 | } 61 | 62 | .message-progress { 63 | color: yellow !important; 64 | fill: yellow !important; 65 | } 66 | 67 | 68 | .top-bar__menu-button { 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | width: 10vh; 73 | height: 10vh; 74 | } 75 | .top-bar__menu-button > svg { 76 | width: 100%; 77 | height: 100%; 78 | } 79 | 80 | .bottom-bar { 81 | height: 8vh; 82 | color: white; 83 | position: relative; 84 | border-top: 2px solid white; 85 | text-align: center; 86 | background: rgba(255, 255, 255, 0.2); 87 | } 88 | 89 | .interface-button, 90 | .interface-button-nohover { 91 | background: none; 92 | border: none; 93 | padding: 2vh; 94 | margin: 0; 95 | } 96 | .interface-button:hover { 97 | background-color: rgba(255, 255, 255, 0.1); 98 | } 99 | .interface-button > svg text { 100 | font-size: 14px; 101 | line-height: 16px; 102 | font-weight: bold; 103 | text-anchor: middle; 104 | fill: white; 105 | } 106 | 107 | /* SVG element classes */ 108 | 109 | .svg-interface-button { 110 | fill: white; 111 | stroke: none; 112 | cursor: pointer; 113 | } 114 | .svg-interface-button-stroke { 115 | /* fill not none, so elements are easily clickable */ 116 | fill: rgba(255, 255, 255, 0.2); 117 | stroke: none; 118 | cursor: pointer; 119 | } 120 | 121 | /* alternatively, some hitbox layer */ 122 | .svg-interface-button-stroke:hover { 123 | fill: rgba(152, 230, 152, 0.3); 124 | } 125 | 126 | .speed:hover > rect.svg-interface-button { 127 | fill: rgba(152, 230, 152, 1) !important; 128 | } 129 | 130 | .svg-interface-text { 131 | fill: white; 132 | cursor: default; 133 | } 134 | .svg-interface-box-stroke { 135 | stroke: none; 136 | fill: rgba(255, 255, 255, 0.2); 137 | } 138 | 139 | .detection-bar-box-stroke { 140 | stroke: white; 141 | stroke-width: 2px; 142 | fill: rgba(255, 255, 255, 0.2); 143 | } 144 | 145 | .view--hidden { 146 | display: none !important; 147 | } 148 | 149 | .view--block { 150 | display: block; 151 | width: 100%; 152 | height: 100%; 153 | } 154 | 155 | /* Level selector */ 156 | 157 | .view--level-selector { 158 | font-size: 12px; 159 | } 160 | 161 | .level-selector { 162 | padding: 10px; 163 | height: 82vh; 164 | box-sizing: border-box; 165 | overflow-y: auto; 166 | } 167 | 168 | .level-selector > ul { 169 | list-style: none; 170 | margin-top: 0; 171 | margin-left: 0; 172 | padding-left: 0; 173 | color: #ddd; 174 | } 175 | 176 | .level-selector > ul > li { 177 | padding-left: 1em; 178 | text-indent: -1em; 179 | list-style: none; 180 | font-size: 3vh; 181 | padding-top: 10px; 182 | cursor: pointer; 183 | } 184 | 185 | .level-selector > ul > li:before { 186 | content: "\269B"; 187 | padding-right: 5px; 188 | } 189 | 190 | .level-selector > ul > li:hover { 191 | color: #fff; 192 | } 193 | 194 | /* Encyclopedia selector */ 195 | 196 | .encyclopedia-selector { 197 | height: 82%; 198 | overflow-y: auto; 199 | } 200 | 201 | .encyclopedia-selector > ul { 202 | list-style: none; 203 | margin: 0; 204 | padding: 0; 205 | color: #ddd; 206 | } 207 | 208 | .encyclopedia-selector > ul > li { 209 | box-sizing: border-box; 210 | display: inline-block; 211 | width: 50%; 212 | padding: 1vh; 213 | overflow: hidden; 214 | } 215 | 216 | .encyclopedia-selector > ul > li button { 217 | width: 100%; 218 | margin: 0; 219 | padding: 0; 220 | background: none; 221 | border: none; 222 | } 223 | 224 | .encyclopedia-selector > ul > li svg { 225 | width: 6vh; 226 | height: 6vh; 227 | background: white; 228 | border: 2px solid #ddd; 229 | margin-right: 2vw; 230 | float: left; 231 | } 232 | 233 | .encyclopedia-selector > ul > li h4 { 234 | display: table-cell; 235 | text-align: left; 236 | vertical-align: middle; 237 | font-size: 2.5vh; 238 | line-height: 2.5vh; 239 | overflow: hidden; 240 | height: 8vh; 241 | margin: 0; 242 | font-weight: bold; 243 | } 244 | 245 | /* Encyclopedia item */ 246 | 247 | .encyclopedia-item { 248 | height: 82%; 249 | } 250 | 251 | .encyclopedia-item__container { 252 | width: 100vh; 253 | max-width: 100vw; 254 | height: 100%; 255 | box-sizing: border-box; 256 | font-size: 2.5vh; 257 | margin: 0 auto; 258 | position: relative; 259 | } 260 | 261 | .encyclopedia-item__container > article { 262 | height: 100%; 263 | overflow-y: auto; 264 | padding: 3vh; 265 | margin-left: 30%; 266 | box-sizing: border-box; 267 | text-align: center; 268 | background: white; 269 | } 270 | .encyclopedia-item__container > ul { 271 | position: absolute; 272 | display: block; 273 | top: 0; 274 | left: 0; 275 | width: 30%; 276 | height: 100%; 277 | box-sizing: border-box; 278 | margin: 0; 279 | padding: 0; 280 | background: rgba(0, 0, 0, 0.3); 281 | color: white; 282 | } 283 | .encyclopedia-item__container > ul li { 284 | list-style: none; 285 | } 286 | .encyclopedia-item__container > ul li button { 287 | background: none; 288 | border: none; 289 | padding: 2vh 1vh; 290 | width: 100%; 291 | } 292 | 293 | .encyclopedia-item__container > article svg.big-tile { 294 | width: 30vh; 295 | } 296 | 297 | .encyclopedia-item__hint { 298 | font-size: 0.8em; 299 | color: #333; 300 | 301 | } 302 | .encyclopedia-item__container svg.transition-heatmap { 303 | width: 70%; 304 | height: 70%; 305 | } 306 | 307 | /* Game */ 308 | 309 | #game { 310 | width: 100%; 311 | height: 100%; 312 | /* Allow to be 100% height */ 313 | font-size: 0; 314 | } 315 | #game > svg.blink-svg { 316 | width: 100%; 317 | height: 100%; 318 | position: absolute; 319 | top: 0; 320 | left: 0; 321 | z-index: 2; 322 | transform: translateZ(0); 323 | } 324 | #game > svg.blink-svg.hidden { 325 | display: none; 326 | } 327 | #game > svg.game-svg { 328 | width: 100%; 329 | height: 100%; 330 | position: relative; 331 | z-index: 3; 332 | } 333 | #game > canvas { 334 | position: fixed; 335 | z-index: 4; 336 | cursor: pointer; 337 | } 338 | #game > canvas.canvas--helper, 339 | #game > canvas.canvas--hidden { 340 | z-index: -1; 341 | visibility: hidden; 342 | } 343 | 344 | .board-controls { 345 | fill: #111; 346 | stroke: none; 347 | } 348 | 349 | .title-bar .hidden { 350 | display: none; 351 | } 352 | 353 | @-webkit-keyframes glowing { 354 | 0% { fill-opacity: 0; } 355 | 50% { fill-opacity: 0.5; } 356 | 100% { fill-opacity: 0; } 357 | } 358 | @-moz-keyframes glowing { 359 | 0% { fill-opacity: 0; } 360 | 50% { fill-opacity: 0.5; } 361 | 100% { fill-opacity: 0; } 362 | } 363 | @-o-keyframes glowing { 364 | 0% { fill-opacity: 0; } 365 | 50% { fill-opacity: 0.5; } 366 | 100% { fill-opacity: 0; } 367 | } 368 | @keyframes glowing { 369 | 0% { fill-opacity: 0; } 370 | 50% { fill-opacity: 0.5; } 371 | 100% { fill-opacity: 0; } 372 | } 373 | 374 | .title-bar .next-level > circle.glowing { 375 | fill: rgb(0, 255, 0); 376 | fill-opacity: 0.2; 377 | -webkit-animation: glowing 1500ms infinite; 378 | -moz-animation: glowing 1500ms infinite; 379 | -o-animation: glowing 1500ms infinite; 380 | animation: glowing 1500ms infinite; 381 | } 382 | 383 | #level-header { 384 | position: fixed; 385 | top: 5px; 386 | width: 100%; 387 | margin: 0 auto; 388 | height: 25px; 389 | text-align: center; 390 | z-index: 5; 391 | } 392 | 393 | #level-footer { 394 | position: fixed; 395 | bottom: 16px; 396 | width: 100%; 397 | margin: 0 auto; 398 | height: 25px; 399 | text-align: center; 400 | z-index: 5; 401 | } 402 | 403 | #element-name { 404 | font-size: 16px; 405 | } 406 | 407 | #element-summary { 408 | font-size: 12px; 409 | } 410 | 411 | #element-flavour { 412 | font-size: 12px; 413 | font-style: italic; 414 | } 415 | 416 | .label-in, .label-out { 417 | font-size: 8px; 418 | } 419 | 420 | .matrix-element { 421 | stroke: gray; 422 | stroke-width: 0.1px; 423 | } 424 | 425 | .matrix-element:hover { 426 | stroke: #000; 427 | stroke-width: 2px; 428 | } 429 | 430 | div.tooltip { 431 | position: fixed; 432 | text-align: left; 433 | padding: 8px; 434 | font: 12px sans-serif; 435 | background: #000; 436 | color: #ddd; 437 | border: solid 1px #aaa; 438 | border-radius: 4px; 439 | pointer-events: none; 440 | z-index: 30; 441 | } 442 | 443 | .shadow-overlay { 444 | position: absolute; 445 | top: 0; 446 | left: 0; 447 | width: 100%; 448 | height: 100%; 449 | z-index: 10; 450 | background-color: rgba(0, 0, 0, 0.4); 451 | cursor: pointer; 452 | } 453 | 454 | .stock .tile text { 455 | font-family: monospace; 456 | font-size: 24px; 457 | font-weight: bold; 458 | fill: #333; 459 | -moz-user-select: -moz-none; 460 | -webkit-user-select: none; 461 | } 462 | .stock .tile.stock--available { 463 | opacity: 1; 464 | } 465 | .stock .tile.stock--depleted { 466 | opacity: 0.5; 467 | } 468 | 469 | .pearl { 470 | cursor: pointer; 471 | } 472 | 473 | .pearl > circle { 474 | fill: rgba(255, 255, 255, 0.2); 475 | stroke: none; 476 | } 477 | .pearl:not(.pearl--passed):hover > circle { 478 | fill: rgba(152, 230, 152, 0.3); 479 | } 480 | .pearl--passed > circle { 481 | fill: rgba(255, 255, 255, 0.6); 482 | } 483 | .pearl--passed:hover > circle { 484 | fill: rgba(152, 230, 152, 0.8); 485 | } 486 | 487 | .pearl > text { 488 | font-size: 20px; 489 | text-anchor: middle; 490 | dominant-baseline: central; 491 | } 492 | .pearl:not(.pearl--current) > text { 493 | opacity: 0.4; 494 | } 495 | .pearl.pearl-current > text { 496 | opacity: 0.9; 497 | } 498 | .pearl:not(.pearl--passed) > text { 499 | fill: #fff; 500 | } 501 | .pearl.pearl-passed > text { 502 | fill: #4f569f; 503 | } 504 | 505 | .helper-hitbox { 506 | fill: lightgreen; 507 | cursor: pointer; 508 | opacity: 0; 509 | } 510 | 511 | .helper-hitbox:hover { 512 | opacity: 0.2; 513 | } 514 | 515 | text.helper-name { 516 | text-anchor: middle; 517 | text-transform: uppercase; 518 | font-size: 24px; 519 | font-family: "Roboto Condensed"; 520 | fill: white; 521 | } 522 | 523 | text.helper-summary { 524 | font-size: 24px; 525 | font-family: "Roboto Condensed"; 526 | fill: white; 527 | } 528 | 529 | .board-hint { 530 | cursor: pointer; 531 | } 532 | 533 | .board-hint > rect { 534 | fill: #4f569f; 535 | fill-opacity: 0.8; 536 | stroke: white; 537 | stroke-width: 2px; 538 | } 539 | 540 | .board-hint > path { 541 | fill: #4f569f; 542 | fill-opacity: 0.8; 543 | stroke: none; 544 | } 545 | 546 | .board-hint > text { 547 | font-size: 24px; 548 | fill: #fff; 549 | text-anchor: middle; 550 | dominant-baseline: middle; 551 | } 552 | 553 | .detection-bar-text { 554 | font-size: 30px; 555 | fill: #fff; 556 | dominant-baseline: middle; 557 | cursor: default; 558 | } 559 | 560 | /* Popup */ 561 | .popup { 562 | position: fixed; 563 | top: 0; 564 | left: 0; 565 | width: 100%; 566 | height: 100%; 567 | overflow: hidden; 568 | z-index: 100; 569 | display: none; 570 | } 571 | 572 | .popup-overlay { 573 | position: absolute; 574 | top: 0; 575 | left: 0; 576 | width: 100%; 577 | height: 100%; 578 | z-index: 1; 579 | background: rgba(0, 0, 0, 0.5); 580 | } 581 | 582 | .popup--shown { 583 | display: table !important; 584 | } 585 | 586 | .popup-container { 587 | display: table-cell; 588 | vertical-align: middle; 589 | } 590 | 591 | .popup-window { 592 | position: relative; 593 | z-index: 10; 594 | width: 50%; 595 | margin: 0 auto; 596 | background: white; 597 | box-shadow: 0 0 10px black; 598 | padding: 2vh; 599 | font-size: 3vh; 600 | font-family: "Roboto Condensed"; 601 | } 602 | 603 | .popup-buttons { 604 | text-align: right; 605 | } 606 | .popup-buttons > button { 607 | background-color: #4f569f; 608 | border: none; 609 | padding: 1vh 3vh; 610 | font-size: 2vh; 611 | color: white; 612 | } 613 | .popup-buttons .hidden { 614 | display: none; 615 | } 616 | 617 | .interface-hint-overlay.hidden { 618 | display: none; 619 | } 620 | .interface-hint-overlay rect { 621 | fill: black; 622 | fill-opacity: 0.7; 623 | } 624 | 625 | .interface-hint-overlay path { 626 | fill: white; 627 | fill-opacity: 0.4; 628 | } 629 | 630 | .interface-hint-overlay text { 631 | fill: white; 632 | font-size: 25px; 633 | text-anchor: middle; 634 | user-select: none; 635 | -moz-user-select: none; 636 | -webkit-user-select: none; 637 | -ms-user-select: none; 638 | } 639 | 640 | .unselectable { 641 | user-select: none; 642 | -moz-user-select: none; 643 | -webkit-user-select: none; 644 | -ms-user-select: none; 645 | } 646 | -------------------------------------------------------------------------------- /css/tiles.css: -------------------------------------------------------------------------------- 1 | rect.background-tile { 2 | fill: #fff; 3 | stroke-width: 4; 4 | stroke: rgba(255, 255, 255, 0.4); 5 | } 6 | 7 | rect.frost-nonfrozen { 8 | opacity: 0; 9 | } 10 | 11 | rect.frost-frozen { 12 | fill: #aaa; 13 | opacity: 0.3; 14 | } 15 | 16 | rect.hitbox { 17 | fill: lightgreen; 18 | cursor: pointer; 19 | opacity: 0; 20 | } 21 | 22 | rect.hitbox-disabled { 23 | fill: #EE9090; 24 | } 25 | 26 | rect.hitbox:hover { 27 | opacity: 0.2; 28 | } 29 | 30 | path.triangular { 31 | fill: #aaa; 32 | stroke: none; 33 | opacity: 0; 34 | } 35 | 36 | path.triangular:hover { 37 | opacity: 1; 38 | } 39 | 40 | .particle { 41 | fill: red; 42 | } 43 | 44 | .beam { 45 | stroke: red; 46 | stroke-width: 1.5px; 47 | } 48 | 49 | .absorption-text { 50 | font-size: 30px; 51 | text-anchor: end; 52 | } 53 | 54 | .element { 55 | cursor: default; 56 | } 57 | 58 | .glass { 59 | fill: steelblue; 60 | fill-opacity: 0.5; 61 | } 62 | .glass-edge { 63 | stroke: steelblue; 64 | stroke-opacity: 1; 65 | stroke-width: 2; 66 | } 67 | 68 | .metal { 69 | fill: #222; 70 | } 71 | 72 | .metal-edge { 73 | stroke: #666; 74 | stroke-width: 2px; 75 | } 76 | 77 | .silver { 78 | fill: lightgrey; 79 | } 80 | 81 | .dark-silver { 82 | fill: #aaa; 83 | } 84 | 85 | .very-dark-silver { 86 | fill: #666; 87 | } 88 | 89 | .absorber { 90 | fill: black; 91 | fill-opacity: 0.4; 92 | stroke: #555; 93 | stroke-width: 1; 94 | } 95 | 96 | .wire { 97 | fill: none; 98 | stroke: #6b00ff; 99 | stroke-width: 3px; 100 | } 101 | 102 | .metal-case { 103 | fill: none; 104 | stroke: #222; 105 | stroke-width: 4px; 106 | } 107 | 108 | .metal-case-hot { 109 | fill: none; 110 | stroke: red; 111 | stroke-width: 4px; 112 | } 113 | 114 | .wire-back { 115 | opacity: 0.5; 116 | stroke-width: 4; 117 | } 118 | 119 | .eye { 120 | stroke: #222; 121 | stroke-width: 3px; 122 | fill: white; 123 | } 124 | 125 | .eye-iris { 126 | stroke: none; 127 | fill: #222; 128 | } 129 | 130 | .lemon { 131 | fill: #ffff00; 132 | stroke: #ffa900; 133 | stroke-width: 3px; 134 | } 135 | 136 | .gold-edge { 137 | stroke: #ffa900; 138 | stroke-width: 2; 139 | } 140 | 141 | .gold-edge-thick { 142 | stroke: #ffa900; 143 | stroke-width: 4; 144 | } 145 | 146 | .polarizer-side { 147 | fill: gray; 148 | fill-opacity: 0.5; 149 | } 150 | 151 | .detector-light-off { 152 | fill: red; 153 | } 154 | 155 | .detector-light-on { 156 | fill: lime; 157 | } 158 | 159 | .measurement-text { 160 | text-anchor: middle; 161 | } 162 | 163 | text.stock-count { 164 | text-anchor: end; 165 | font-size: 24px; 166 | stroke: #222; 167 | } 168 | 169 | .stock-empty > g.stock-dragged { 170 | opacity: 1 !important; 171 | } 172 | 173 | .stock-empty > g > use.element, .stock-empty > text.stock-count { 174 | opacity: 0.5; 175 | } 176 | 177 | .stock-empty > g > .hitbox { 178 | fill: #EE9090; 179 | } 180 | -------------------------------------------------------------------------------- /data/levels_concepts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Quantum suicide", 4 | "group": "Concept", 5 | "width": 13, 6 | "height": 10, 7 | "tiles": [ 8 | {"i": 4, "j": 3, "name": "Source", "rotation": 0, "frozen": false}, 9 | {"i": 7, "j": 3, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 10 | {"i": 7, "j": 6, "name": "Mine", "rotation": 0, "frozen": false}, 11 | {"i": 10, "j": 3, "name": "Detector", "rotation": 0, "frozen": false} 12 | ] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /data/levels_last.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "No more challenges", 3 | "group": "Last", 4 | "width": 13, 5 | "height": 10, 6 | "tiles": [ 7 | {"i": 10, "j": 1, "name": "Source", "rotation": 0, "frozen": true}, 8 | {"i": 11, "j": 1, "name": "Detector", "rotation": 0, "frozen": true} 9 | ], 10 | "stock": {}, 11 | "boardHints": [ 12 | {"i": 4, "j": 0, "widthI": 5, "text": "thank you for testing!"}, 13 | {"i": 5, "j": 1, "widthI": 3, "text": "(it's still beta)"}, 14 | {"i": 4, "j": 2, "widthI": 5, "text": "we really hope you enjoyed it a bit"}, 15 | {"i": 9, "j": 5, "widthI": 1, "text": "by:"}, 16 | {"i": 9, "j": 6, "widthI": 3, "text": "Piotr Migdał"}, 17 | {"i": 9, "j": 7, "widthI": 3, "text": "Patryk Hes"}, 18 | {"i": 9, "j": 8, "widthI": 3, "text": "Michał Krupiński"}, 19 | {"i": 0, "j": 3, "widthI": 3, "text": "all missions over...", "triangleI": 0, "triangleDir": "left"}, 20 | {"i": 0, "j": 4, "widthI": 4, "text": "...but we can make more..,"}, 21 | {"i": 0, "j": 5, "widthI": 4, "text": "...and you can as well!"}, 22 | {"i": 0, "j": 9, "widthI": 10, "text": "stay with us... :) at tell us what you enjoyed, and what was annoying)", "triangleI": 0, "triangleDir": "left"} 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /data/levels_other.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Sugar vs mirrors", 4 | "group": "X Examples", 5 | "width": 13, 6 | "height": 10, 7 | "stock": {}, 8 | "tiles": [ 9 | {"i": 3, "j": 3, "name": "Source", "rotation": 0, "frozen": false}, 10 | {"i": 5, "j": 5, "name": "PolarizingSplitter", "rotation": 0, "frozen": false}, 11 | {"i": 6, "j": 3, "name": "SugarSolution", "rotation": 0, "frozen": false}, 12 | {"i": 8, "j": 5, "name": "SugarSolution", "rotation": 0, "frozen": false}, 13 | {"i": 9, "j": 3, "name": "ThinMirror", "rotation": 3, "frozen": false}, 14 | {"i": 9, "j": 5, "name": "ThinMirror", "rotation": 1, "frozen": false} 15 | ] 16 | }, 17 | { 18 | "name": "So close yet so far", 19 | "group": "X Playing", 20 | "width": 13, 21 | "height": 10, 22 | "tiles": [ 23 | {"i": 0, "j": 2, "name": "Source", "rotation": 0, "frozen": false}, 24 | {"i": 1, "j": 1, "name": "Detector", "rotation": 1, "frozen": false}, 25 | {"i": 1, "j": 2, "name": "PolarizingSplitter", "rotation": 1, "frozen": false}, 26 | {"i": 3, "j": 2, "name": "SugarSolution", "rotation": 0, "frozen": false}, 27 | {"i": 5, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 28 | {"i": 5, "j": 3, "name": "Glass", "rotation": 0, "frozen": false}, 29 | {"i": 5, "j": 4, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 30 | {"i": 5, "j": 6, "name": "ThinMirror", "rotation": 3, "frozen": false}, 31 | {"i": 7, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 32 | {"i": 7, "j": 6, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 33 | {"i": 9, "j": 2, "name": "ThinMirror", "rotation": 3, "frozen": false}, 34 | {"i": 9, "j": 4, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 35 | {"i": 9, "j": 6, "name": "ThinMirror", "rotation": 1, "frozen": false} 36 | ] 37 | }, 38 | { 39 | "name": "Mirrors and polarization - not sure", 40 | "group": "X Test", 41 | "texts": {"before": "Try moving sugar solution - it will cancel (not sure if its OK)"}, 42 | "width": 13, 43 | "height": 10, 44 | "stock": {}, 45 | "tiles": [ 46 | {"i": 1, "j": 2, "name": "Source", "rotation": 0, "frozen": false}, 47 | {"i": 3, "j": 2, "name": "PolarizingSplitter", "rotation": 1, "frozen": false}, 48 | {"i": 4, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 49 | {"i": 4, "j": 6, "name": "ThinMirror", "rotation": 3, "frozen": false}, 50 | {"i": 6, "j": 2, "name": "SugarSolution", "rotation": 0, "frozen": false}, 51 | {"i": 6, "j": 6, "name": "SugarSolution", "rotation": 0, "frozen": false}, 52 | {"i": 8, "j": 2, "name": "ThinMirror", "rotation": 3, "frozen": false}, 53 | {"i": 8, "j": 6, "name": "ThinMirror", "rotation": 1, "frozen": false} 54 | ] 55 | }, 56 | { 57 | "name": "Geometrical series - detection", 58 | "group": "X Test", 59 | "width": 13, 60 | "height": 10, 61 | "stock": {}, 62 | "tiles": [ 63 | {"i": 3, "j": 3, "name": "Source", "rotation": 0, "frozen": false}, 64 | {"i": 6, "j": 1, "name": "Detector", "rotation": 1, "frozen": false}, 65 | {"i": 6, "j": 3, "name": "ThinSplitter", "rotation": 1, "frozen": false}, 66 | {"i": 6, "j": 5, "name": "ThinMirror", "rotation": 3, "frozen": false}, 67 | {"i": 8, "j": 3, "name": "ThinMirror", "rotation": 3, "frozen": false}, 68 | {"i": 8, "j": 5, "name": "ThinMirror", "rotation": 1, "frozen": false} 69 | ] 70 | }, 71 | { 72 | "name": "Geometrical series - train", 73 | "group": "X Test", 74 | "width": 13, 75 | "height": 10, 76 | "stock": {}, 77 | "tiles": [ 78 | {"i": 0, "j": 0, "name": "ThinMirror", "rotation": 1, "frozen": false}, 79 | {"i": 0, "j": 9, "name": "Detector", "rotation": 3, "frozen": false}, 80 | {"i": 1, "j": 2, "name": "Source", "rotation": 0, "frozen": false}, 81 | {"i": 2, "j": 1, "name": "ThinMirror", "rotation": 1, "frozen": false}, 82 | {"i": 2, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 83 | {"i": 2, "j": 9, "name": "ThinMirror", "rotation": 3, "frozen": false}, 84 | {"i": 3, "j": 1, "name": "ThinMirror", "rotation": 3, "frozen": false}, 85 | {"i": 3, "j": 2, "name": "ThinMirror", "rotation": 1, "frozen": false}, 86 | {"i": 12, "j": 0, "name": "ThinMirror", "rotation": 3, "frozen": false}, 87 | {"i": 12, "j": 9, "name": "ThinMirror", "rotation": 1, "frozen": false} 88 | ] 89 | }, 90 | { 91 | "name": "Polarization fun", 92 | "group": "X Various", 93 | "width": 13, 94 | "height": 10, 95 | "stock": {}, 96 | "tiles": [ 97 | {"i": 1, "j": 3, "name": "Source", "rotation": 0, "frozen": false}, 98 | {"i": 2, "j": 3, "name": "SugarSolution", "rotation": 0, "frozen": false}, 99 | {"i": 3, "j": 3, "name": "SugarSolution", "rotation": 0, "frozen": false}, 100 | {"i": 4, "j": 3, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 101 | {"i": 4, "j": 4, "name": "ThinMirror", "rotation": 3, "frozen": false}, 102 | {"i": 5, "j": 2, "name": "ThinMirror", "rotation": 1, "frozen": false}, 103 | {"i": 5, "j": 3, "name": "ThinMirror", "rotation": 1, "frozen": false}, 104 | {"i": 5, "j": 4, "name": "SugarSolution", "rotation": 0, "frozen": false}, 105 | {"i": 6, "j": 3, "name": "ThinMirror", "rotation": 3, "frozen": false}, 106 | {"i": 6, "j": 4, "name": "SugarSolution", "rotation": 0, "frozen": false}, 107 | {"i": 7, "j": 3, "name": "PolarizingSplitter", "rotation": 0, "frozen": false}, 108 | {"i": 7, "j": 4, "name": "ThinMirror", "rotation": 1, "frozen": false}, 109 | {"i": 8, "j": 3, "name": "QuarterWavePlate", "rotation": 1, "frozen": false}, 110 | {"i": 9, "j": 1, "name": "Mine", "rotation": 0, "frozen": false}, 111 | {"i": 9, "j": 3, "name": "PolarizingSplitter", "rotation": 0, "frozen": false}, 112 | {"i": 11, "j": 3, "name": "Detector", "rotation": 0, "frozen": false} 113 | ] 114 | }, 115 | { 116 | "name": "Lost in the BS woods (99% detection)", 117 | "group": "X Test", 118 | "width": 13, 119 | "height": 10, 120 | "tiles": [ 121 | {"i": 0, "j": 2, "name": "Source", "rotation": 0, "frozen": false}, 122 | {"i": 1, "j": 2, "name": "SugarSolution", "rotation": 0, "frozen": false}, 123 | {"i": 2, "j": 2, "name": "SugarSolution", "rotation": 0, "frozen": false}, 124 | {"i": 3, "j": 2, "name": "PolarizingSplitter", "rotation": 0, "frozen": false}, 125 | {"i": 3, "j": 4, "name": "Detector", "rotation": 3, "frozen": false}, 126 | {"i": 4, "j": 2, "name": "FaradayRotator", "rotation": 0, "frozen": false}, 127 | {"i": 4, "j": 3, "name": "ThinMirror", "rotation": 2, "frozen": false}, 128 | {"i": 4, "j": 4, "name": "ThinMirror", "rotation": 2, "frozen": false}, 129 | {"i": 5, "j": 1, "name": "ThinMirror", "rotation": 0, "frozen": false}, 130 | {"i": 5, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 131 | {"i": 5, "j": 3, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 132 | {"i": 5, "j": 4, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 133 | {"i": 5, "j": 5, "name": "ThinMirror", "rotation": 0, "frozen": false}, 134 | {"i": 6, "j": 1, "name": "ThinMirror", "rotation": 0, "frozen": false}, 135 | {"i": 6, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 136 | {"i": 6, "j": 3, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 137 | {"i": 6, "j": 4, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 138 | {"i": 6, "j": 5, "name": "ThinMirror", "rotation": 0, "frozen": false}, 139 | {"i": 7, "j": 1, "name": "ThinMirror", "rotation": 0, "frozen": false}, 140 | {"i": 7, "j": 2, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 141 | {"i": 7, "j": 3, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 142 | {"i": 7, "j": 4, "name": "ThinSplitter", "rotation": 3, "frozen": false}, 143 | {"i": 7, "j": 5, "name": "ThinMirror", "rotation": 0, "frozen": false}, 144 | {"i": 8, "j": 2, "name": "ThinMirror", "rotation": 2, "frozen": false}, 145 | {"i": 8, "j": 3, "name": "ThinMirror", "rotation": 2, "frozen": false}, 146 | {"i": 8, "j": 4, "name": "ThinMirror", "rotation": 2, "frozen": false} 147 | ] 148 | }, 149 | { 150 | "name": "Testing first-win animation", 151 | "group": "X Test", 152 | "i": 1, 153 | "next": "Game Introducing mirrors", 154 | "width": 13, 155 | "height": 10, 156 | "tiles": [ 157 | {"i": 2, "j": 1, "name": "Source", "rotation": 0, "frozen": true}, 158 | {"i": 4, "j": 1, "name": "ThinSplitter", "rotation": 3, "frozen": true}, 159 | {"i": 4, "j": 2, "name": "Rock", "rotation": 0, "frozen": true}, 160 | {"i": 5, "j": 1, "name": "ThinSplitter", "rotation": 3, "frozen": true}, 161 | {"i": 5, "j": 4, "name": "Rock", "rotation": 0, "frozen": true}, 162 | {"i": 6, "j": 1, "name": "Absorber", "rotation": 0, "frozen": true}, 163 | {"i": 10, "j": 1, "name": "ThinSplitter", "rotation": 3, "frozen": true}, 164 | {"i": 10, "j": 3, "name": "DetectorFour", "rotation": 0, "frozen": true}, 165 | {"i": 11, "j": 1, "name": "Detector", "rotation": 0, "frozen": true} 166 | ], 167 | "requiredDetectionProbability": 0.01, 168 | "detectorsToFeed": 0, 169 | "texts": {"before": "Adventures of a Curious Character"} 170 | } 171 | ] 172 | -------------------------------------------------------------------------------- /deploy_dev.sh: -------------------------------------------------------------------------------- 1 | # the only other step is updating index.html file! 2 | jspm bundle-sfx --minify app.js 3 | rm bundled/build.js* 4 | mv build.js* bundled/ 5 | s3cmd sync bundled/index.html s3://quantumgame.io/dev/ 6 | s3cmd sync bundled/build.js s3://quantumgame.io/dev/ 7 | s3cmd sync --recursive css s3://quantumgame.io/dev/ 8 | s3cmd sync --recursive sounds s3://quantumgame.io/ 9 | s3cmd sync bg.jpg s3://quantumgame.io/dev/ 10 | s3cmd sync logo.svg s3://quantumgame.io/dev/ 11 | s3cmd sync favicon.ico s3://quantumgame.io/dev/ 12 | -------------------------------------------------------------------------------- /deploy_play.sh: -------------------------------------------------------------------------------- 1 | # the only other step is updating index.html file! 2 | jspm bundle-sfx --minify app.js 3 | rm bundled/build.js* 4 | mv build.js* bundled/ 5 | s3cmd sync bundled/index.html s3://play.quantumgame.io/ 6 | s3cmd sync bundled/build.js s3://play.quantumgame.io/ 7 | s3cmd sync --recursive css s3://play.quantumgame.io/ 8 | s3cmd sync --recursive sounds s3://play.quantumgame.io/ 9 | s3cmd sync bg.jpg s3://play.quantumgame.io/ 10 | s3cmd sync logo.svg s3://play.quantumgame.io/ 11 | s3cmd sync favicon.ico s3://play.quantumgame.io/ 12 | s3cmd sync screenshot_qg_dev.png s3://play.quantumgame.io/ 13 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/favicon.ico -------------------------------------------------------------------------------- /js/bare_board.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import d3 from 'd3'; 3 | 4 | import {tileSize, tileBorder, animationStepDuration} from './config'; 5 | import {CanvasParticleAnimation} from './particle/canvas_particle_animation'; 6 | import * as simulation from './simulation'; 7 | import * as tile from './tile'; 8 | import {WinningStatus} from './winning_status'; 9 | import {bindDrag} from './drag_and_drop'; 10 | import {Logger} from './logger'; 11 | import {SoundService} from './sound_service'; 12 | 13 | export class BareBoard { 14 | constructor(svg, gameBoard, drawMode = 'orthogonal', measurementMode = 'measurement: Copenhagen', margin = {}, callbacks = {}) { 15 | this.svg = svg; 16 | this.gameBoard = gameBoard; 17 | // TODO: refactor as it is being changed remotly 18 | this.drawMode = drawMode; 19 | this.measurementMode = measurementMode; 20 | 21 | this.margin = margin; 22 | this.tileMatrix = []; 23 | this.animationStepDuration = animationStepDuration; 24 | 25 | // NOTE maybe some event listener instead? 26 | this.callbacks = { 27 | tileRotated: callbacks.tileRotated || _.noop, 28 | tileMouseover: callbacks.tileMouseover || _.noop, 29 | animationStart: callbacks.animationStart || _.noop, 30 | animationInterrupt: callbacks.animationInterrupt || _.noop, 31 | animationEnd: callbacks.animationEnd || _.noop, 32 | setPlayButtonState: callbacks.setPlayButtonState || _.noop, 33 | }; 34 | 35 | this.logger = new Logger(); 36 | this.logger.logAction('initialLevel'); 37 | 38 | // this field is modified by ParticleAnimation 39 | this.animationExists = false; 40 | } 41 | 42 | redraw() { 43 | // set tileMatrix according to the recipe 44 | this.clearTileMatrix(); 45 | this.fillTileMatrix(this.level.tileRecipes); 46 | 47 | // works both as initial drawing and redrawing 48 | this.resizeSvg(); 49 | this.drawBackground(); 50 | this.drawBoardHints(); 51 | this.drawBoard(); 52 | } 53 | 54 | clearTileMatrix() { 55 | // Create matrix filled with Vacuum 56 | this.tileMatrix = _.range(this.level.width).map((i) => 57 | _.range(this.level.height).map((j) => 58 | new tile.Tile(tile.Vacuum, 0, false, i, j) 59 | ) 60 | ); 61 | } 62 | 63 | fillTileMatrix(tileRecipes) { 64 | _.each(tileRecipes, (tileRecipe) => { 65 | this.tileMatrix[tileRecipe.i][tileRecipe.j] = new tile.Tile( 66 | tile[tileRecipe.name], 67 | tileRecipe.rotation || 0, 68 | !!tileRecipe.frozen, 69 | tileRecipe.i, 70 | tileRecipe.j 71 | ); 72 | }); 73 | } 74 | 75 | resizeSvg() { 76 | const top = this.margin.top || 0; 77 | const left = this.margin.left || 0; 78 | const bottom = this.margin.bottom || 0; 79 | const right = this.margin.right || 0; 80 | // Use margin to calculate effective size 81 | const width = this.level.width + left + right; 82 | const height = this.level.height + top + bottom; 83 | // min-x, min-y, width and height 84 | this.svg.attr('viewBox', `${-tileSize * left} ${-tileSize * top} ${tileSize * width} ${tileSize * height}`); 85 | } 86 | 87 | /** 88 | * Draw background - a grid of squares. 89 | */ 90 | drawBackground() { 91 | 92 | this.svg.select('.background').remove(); 93 | 94 | this.svg 95 | .append('g') 96 | .attr('class', 'background') 97 | .selectAll('.background-tile') 98 | .data(_.chain(this.tileMatrix) // NOTE I cannot just clone due to d.x and d.y getters 99 | .flatten() 100 | .map((d) => new tile.Tile(d.type, d.rotation, d.frozen, d.i, d.j)) 101 | .value() 102 | ) 103 | .enter() 104 | .append('rect') 105 | .attr({ 106 | 'class': 'background-tile', 107 | x: (d) => d.x + tileBorder, 108 | y: (d) => d.y + tileBorder, 109 | width: tileSize - 2 * tileBorder, 110 | height: tileSize - 2 * tileBorder, 111 | }); 112 | } 113 | 114 | drawBoardHints() { 115 | 116 | const tipMargin = tileSize / 4; 117 | 118 | this.svg.select('.board-hints').remove(); 119 | 120 | this.boardHints = this.svg.append('g') 121 | .attr('class', 'board-hints') 122 | .selectAll('.board-hint') 123 | .data(this.level.boardHints) 124 | .enter().append('g') 125 | .attr('class', 'board-hint') 126 | .attr('transform', (d) => 127 | `translate(${tileSize * d.i + tipMargin},${tileSize * d.j + tipMargin})` 128 | ) 129 | .on('click', function () { 130 | d3.select(this) 131 | .style('opacity', 1) 132 | .transition().duration(animationStepDuration) 133 | .style('opacity', 0); 134 | }); 135 | 136 | this.boardHints.append('rect') 137 | .attr('x', 0) 138 | .attr('y', 0) 139 | .attr('width', (d) => d.widthI * tileSize - 2 * tipMargin) 140 | .attr('height', tileSize - 2 * tipMargin); 141 | 142 | this.boardHints.append('text') 143 | .attr('x', (d) => d.widthI * tileSize / 2 - tipMargin) 144 | .attr('y', tileSize / 2 - tipMargin) 145 | .text((d) => d.text); 146 | 147 | // Triangle size unit 148 | const t = tileSize / 4; 149 | // Traingle dir to rotation 150 | const dirToRot = { 151 | bottom: 0, 152 | left: 90, 153 | top: 180, 154 | right: 270, 155 | }; 156 | 157 | // Board hint can have a triangle tip (like in dialogue balloon) 158 | this.boardHints.filter((d) => d.triangleI != null) 159 | .append('path') 160 | .attr('d', `M${-t/2} 0 L0 ${t} L${t/2} 0 Z`) 161 | .attr('transform', (d) => `translate(${(d.triangleI - d.i) * tileSize + t}, ${t}) rotate(${dirToRot[d.triangleDir]}) translate(0, ${t})`); 162 | 163 | } 164 | 165 | /** 166 | * Draw board: tiles and their hitboxes. 167 | * Also, bind click and drag events. 168 | */ 169 | drawBoard() { 170 | 171 | this.svg.select('.board').remove(); 172 | this.boardGroup = this.svg 173 | .append('g') 174 | .attr('class', 'board'); 175 | 176 | _.flatten(this.tileMatrix) 177 | .filter((t) => t.type !== tile.Vacuum) 178 | .forEach((t) => this.addTile(t)); 179 | } 180 | 181 | addTile(tileObj) { 182 | 183 | this.removeTile(tileObj.i, tileObj.j); 184 | this.tileMatrix[tileObj.i][tileObj.j] = tileObj; 185 | 186 | const tileSelection = this.boardGroup 187 | .datum(tileObj) 188 | .append('g') 189 | .attr('class', 'tile') 190 | .attr('transform', (d) => `translate(${d.x + tileSize / 2},${d.y + tileSize / 2})`); 191 | 192 | tileObj.g = tileSelection; 193 | // DOM element for g 194 | tileObj.node = tileSelection[0][0]; 195 | 196 | // frozen background 197 | tileSelection 198 | .append('rect') 199 | .attr('class', (d) => d.frozen ? 'frost frost-frozen' : 'frost frost-nonfrozen') 200 | .attr('x', -tileSize / 2) 201 | .attr('y', -tileSize / 2) 202 | .attr('width', tileSize) 203 | .attr('height', tileSize); 204 | 205 | tileObj.draw(); 206 | 207 | // hitbox 208 | tileSelection 209 | .append('rect') 210 | .attr('class', 'hitbox') 211 | .attr('x', -tileSize / 2) 212 | .attr('y', -tileSize / 2) 213 | .attr('width', tileSize) 214 | .attr('height', tileSize); 215 | 216 | this.clickBehavior(tileSelection, this); 217 | bindDrag(tileSelection, this, this.stock); 218 | 219 | } 220 | 221 | removeTile(i, j) { 222 | if (this.tileMatrix[i][j].node) { 223 | this.tileMatrix[i][j].node.remove(); 224 | } 225 | this.tileMatrix[i][j] = new tile.Tile(tile.Vacuum, 0, false, i, j); 226 | } 227 | 228 | clickBehavior(tileSelection, bareBoard) { 229 | tileSelection.select('.hitbox').on('click', (d) => { 230 | 231 | // Avoid rotation when dragged 232 | if (d3.event.defaultPrevented) { 233 | return; 234 | } 235 | 236 | // Avoid rotation when frozen 237 | if (d.frozen) { 238 | if (d.tileName === 'Source') { 239 | this.logger.logAction('play', {clickingSource: true}); 240 | bareBoard.play(); 241 | } else { 242 | // Do nothing on the board - only play the sound 243 | SoundService.playThrottled('error'); 244 | } 245 | return; 246 | } 247 | 248 | if (bareBoard.animationExists) { 249 | this.logger.logAction('simulationStop', {cause: 'click on element'}); 250 | bareBoard.stop(); 251 | bareBoard.callbacks.animationInterrupt(); 252 | } 253 | 254 | d.rotate(); 255 | SoundService.playThrottled('blip'); 256 | this.logger.logAction('rotate', {name: d.tileName, i: d.i, j: d.j, toRotation: d.rotation}); 257 | bareBoard.callbacks.tileRotated(d); 258 | 259 | }) 260 | .on('mouseover', function (d) { 261 | bareBoard.callbacks.tileMouseover(d); 262 | d3.select(this).classed('hitbox-disabled', d.frozen); 263 | }); 264 | 265 | // this is a tricky part 266 | // freeze/unfreeze traingular button 267 | // FIX allow adding it later 268 | if (this.level.group === 'A Dev') { 269 | tileSelection 270 | .append('path') 271 | .attr('class', 'triangular') 272 | .attr('d', 'M 0 0 L -1 0 L 0 1 Z') 273 | .attr('transform', `translate(${tileSize / 2},${-tileSize / 2}) scale(${tileSize / 4})`) 274 | .on('click', (d) => { 275 | d.frozen = !d.frozen; 276 | this.logger.logAction('changeFreeze', {name: d.tileName, i: d.i, j: d.j, toFrozen: d.frozen}); 277 | d.g.select('.frost') 278 | .attr('class', d.frozen ? 'frost frost-frozen' : 'frost frost-nonfrozen'); 279 | }); 280 | } 281 | } 282 | 283 | 284 | /** 285 | * Generate history. 286 | */ 287 | generateHistory() { 288 | 289 | this.winningStatus = new WinningStatus(this.tileMatrix); 290 | this.winningStatus.run(); 291 | if (this.level.group === 'Game') { 292 | this.winningStatus.compareToObjectives( 293 | this.level.requiredDetectionProbability, 294 | this.level.detectorsToFeed 295 | ); 296 | } else { 297 | this.winningStatus.isWon = false; 298 | this.winningStatus.message = 'No goals, no judgement.'; 299 | // "Wszystko wolno - hulaj dusza 300 | // Do niczego się nie zmuszaj!" 301 | // "Nie planować i nie marzyć 302 | // Co się zdarzy to się zdarzy. 303 | // Nie znać dobra ani zła 304 | // To jest gra i tylko gra!" 305 | } 306 | window.console.log(this.winningStatus); 307 | 308 | // 'improved' history for the first win 309 | const firstWin = this.winningStatus.isWon && !this.alreadyWon; 310 | this.alreadyWon = this.alreadyWon || this.winningStatus.isWon; 311 | 312 | // non-deterministic quantum simulation 313 | // (for animations) 314 | this.simulationQ = new simulation.Simulation(this.tileMatrix, 'logging'); 315 | this.simulationQ.initialize(); 316 | 317 | if (this.measurementMode == 'Copenhagen') { 318 | if (firstWin && this.winningStatus.totalProbAtDets > 0) { 319 | // TO DO - to fix! 320 | this.simulationQ.propagateToEndCheated(this.winningStatus.probsAtDetsByTime); 321 | } else { 322 | this.simulationQ.propagateToEnd(true); 323 | } 324 | } else { 325 | this.simulationQ.propagateToEnd(false); 326 | } 327 | 328 | this.logger.logAction('run', { 329 | isWon: this.winningStatus.isWon, 330 | enoughProbability: this.winningStatus.enoughProbability, 331 | totalProbAtDets: this.winningStatus.totalProbAtDets, 332 | enoughDetectors: this.winningStatus.enoughDetectors, 333 | noOfFedDets: this.winningStatus.noOfFedDets, 334 | noExplosion: this.winningStatus.noExplosion, 335 | probsAtMines: this.winningStatus.probsAtMines, 336 | }); 337 | 338 | } 339 | 340 | /** 341 | * Generate history and animation. 342 | */ 343 | generateAnimation() { 344 | if (this.animationExists) { 345 | this.particleAnimation.stop(); 346 | } 347 | this.generateHistory(); 348 | this.particleAnimation = new CanvasParticleAnimation( 349 | this, 350 | this.simulationQ.history, 351 | this.simulationQ.measurementHistory, 352 | this.winningStatus.absorptionProbabilities, 353 | this.callbacks.animationInterrupt, 354 | this.callbacks.animationEnd, 355 | this.drawMode, 356 | (s) => this.gameBoard.titleManager.displayMessage(s, 'progress', -1) 357 | ); 358 | } 359 | 360 | /** 361 | * Play animation. Generate history if necessary. 362 | */ 363 | // TODO simplify its logic? 364 | play() { 365 | this.logger.logAction('simulationPlay'); 366 | this.callbacks.animationStart(); 367 | if (!this.animationExists) { 368 | this.generateAnimation(); 369 | } 370 | // After generation, this.animationExists is true 371 | if (this.particleAnimation.playing) { 372 | this.particleAnimation.pause(); 373 | this.callbacks.setPlayButtonState('play'); 374 | } else { 375 | this.particleAnimation.play(); 376 | this.callbacks.setPlayButtonState('pause'); 377 | } 378 | } 379 | 380 | stop() { 381 | this.logger.logAction('simulationStop'); 382 | if (this.animationExists) { 383 | this.particleAnimation.stop(); 384 | this.callbacks.setPlayButtonState('play'); 385 | } 386 | } 387 | 388 | forward() { 389 | if (!this.animationExists) { 390 | this.generateAnimation(); 391 | this.particleAnimation.initialize(); 392 | } 393 | // After generation, this.animationExists is true 394 | if (this.particleAnimation.playing) { 395 | this.particleAnimation.pause(); 396 | this.callbacks.setPlayButtonState('play'); 397 | } else { 398 | this.particleAnimation.forward(); 399 | } 400 | } 401 | 402 | // NOTE maybe only exporting some 403 | exportBoard() { 404 | // should match interface from level.js 405 | return { 406 | name: this.level.name, 407 | group: this.level.group, 408 | id: this.level.id, 409 | i: this.level.i, 410 | next: this.level.next, 411 | width: this.level.width, 412 | height: this.level.height, 413 | tiles: _.chain(this.tileMatrix) 414 | .flatten() 415 | .filter((d) => d.tileName !== 'Vacuum') 416 | .map((d) => ({ 417 | i: d.i, 418 | j: d.j, 419 | name: d.tileName, 420 | rotation: d.rotation, 421 | frozen: d.frozen, 422 | })) 423 | .value(), 424 | stock: this.stock ? this.stock.stock : {}, // hack for non-attached stock 425 | requiredDetectionProbability: this.level.requiredDetectionProbability, 426 | detectorsToFeed: this.level.detectorsToFeed, 427 | texts: this.level.texts, 428 | initialHint: this.level.initialHint, 429 | boardHints: this.level.boardHints, 430 | }; 431 | } 432 | 433 | } 434 | -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | // Tile size (px) 2 | export const tileSize = 100; 3 | // Tile border (px) 4 | export const tileBorder = 1; 5 | // Rotation speed (ms) 6 | export const rotationSpeed = 125; 7 | // Tile reposition speed (ms) 8 | export const repositionSpeed = 125; 9 | // Maximum iteration count 10 | export const maxIterations = 1000; 11 | // Default animation step duration (ms) 12 | export const animationStepDuration = 500; 13 | // Min animation step duration - for slider (ms) 14 | export const animationStepDurationMin = 100; 15 | // Max animation step duration - for slider (ms) 16 | export const animationStepDurationMax = 2000; 17 | // Play/pause button transition duration 18 | export const playPauseTransitionDuration = 300; 19 | // Oscillations per tile 20 | export const oscillations = 1; 21 | // Horizontal oscillation scale (px) 22 | export const polarizationScaleH = 15; 23 | // Vertical oscillation scale (factor) 24 | export const polarizationScaleV = 0.7; 25 | // Canvas resize throttling (ms) 26 | export const resizeThrottle = 100; 27 | // How often we should draw particles on canvas, measured in light units. 28 | // Example: when set to 20, there should be 20 drawings of dot every time 29 | // when photon travels one tile. 30 | export const canvasDrawFrequency = 20; 31 | // Absorption animation duration (ms) 32 | export const absorptionDuration = 2000; 33 | // Absorption test duration (ms) 34 | export const absorptionTextDuration = 8000; 35 | // Display message default timeout (ms) 36 | export const displayMessageTimeout = 3000; 37 | // Pearls per column 38 | export const pearlsPerRow = 3; 39 | // Maximal number of stock columns (for determining interface size) 40 | export const stockColumns = 5; 41 | // Stock height (in tiles) 42 | export const stockHeight = 4; 43 | // Tile helper size (in tiles) 44 | export const tileHelperWidth = 4; 45 | export const tileHelperHeight = 3; 46 | // Is production? 47 | export const isProduction = document.URL.indexOf('play.quantumgame.io') !== -1; 48 | -------------------------------------------------------------------------------- /js/const.js: -------------------------------------------------------------------------------- 1 | export const TAU = 2 * Math.PI; 2 | export const EPSILON = 1e-5; 3 | // for level-winning conditions 1% seems to be fine 4 | export const EPSILON_DETECTION = 0.01; 5 | export const velocityI = { 6 | '>': 1, 7 | '^': 0, 8 | '<': -1, 9 | 'v': 0, 10 | }; 11 | export const velocityJ = { 12 | '>': 0, 13 | '^': -1, // TODO when changing (i,j) to cartesian, change it to 1 14 | '<': 0, 15 | 'v': 1, // TODO when changing (i,j) to cartesian, change it to -1 16 | }; 17 | 18 | // also changes for cartesian 19 | // with non-cartesian perhaps its broken anyways :) 20 | export const perpendicularI = { 21 | '>': 0, 22 | '^': -1, 23 | '<': 0, 24 | 'v': 1, 25 | }; 26 | export const perpendicularJ = { 27 | '>': -1, 28 | '^': 0, 29 | '<': 1, 30 | 'v': 0, 31 | }; 32 | -------------------------------------------------------------------------------- /js/detection_bar.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import _ from 'lodash'; 3 | 4 | import {tileSize, absorptionDuration} from './config'; 5 | 6 | const barHeight = tileSize / 3; 7 | const barWidth = 2 * tileSize; 8 | const textMargin = 10; 9 | 10 | const percentStr = (probability) => 11 | (100 * probability).toFixed(1) 12 | 13 | export class DetectionBar { 14 | constructor(svg) { 15 | this.g = svg.append('g') 16 | .attr('class', 'detection-bar'); 17 | this.draw(); 18 | } 19 | 20 | draw() { 21 | 22 | // 23 | // percent group 24 | // 25 | this.percentG = this.g.append('g'); 26 | 27 | this.percentScale = d3.scale.linear() 28 | .domain([0, 1]) 29 | .range([0, barWidth]); 30 | 31 | this.percentActual = this.percentG.append('rect') 32 | .attr('x', 0) 33 | .attr('y', 0) 34 | .attr('width', 0) 35 | .attr('height', barHeight) 36 | .style('fill', '#0a0') 37 | .style('stroke', 'none'); 38 | 39 | this.percentRequired = this.percentG.append('rect') 40 | .attr('class', 'detection-bar-box-stroke') 41 | .attr('x', 0) 42 | .attr('y', 0) 43 | .attr('width', 0) 44 | .attr('height', barHeight); 45 | 46 | // border 47 | this.percentG.append('rect') 48 | .attr('class', 'detection-bar-box-stroke') 49 | .attr('x', 0) 50 | .attr('y', 0) 51 | .attr('width', barWidth) 52 | .attr('height', barHeight) 53 | .style('fill', 'none'); 54 | 55 | this.percentText = this.percentG.append('text') 56 | .attr('class', 'detection-bar-text') 57 | .attr('x', barWidth + textMargin) 58 | .attr('y', barHeight / 2); 59 | 60 | // 61 | // count group 62 | // 63 | this.countG = this.g.append('g') 64 | .attr('transform', `translate(${7 * tileSize},0)`); 65 | 66 | this.detectorsText = this.countG.append('text') 67 | .attr('class', 'detection-bar-text') 68 | .attr('y', barHeight / 2) 69 | .text('detectors'); 70 | 71 | // 72 | // mine group 73 | // 74 | this.mineG = this.g.append('g') 75 | .attr('transform', `translate(${10.5 * tileSize},0)`); 76 | 77 | this.mineBox = this.mineG.append('rect') 78 | .attr('class', 'mine-box detection-bar-box-stroke') 79 | .attr('x', 0) 80 | .attr('y', 0) 81 | .attr('width', barHeight / 2) 82 | .attr('height', barHeight) 83 | .style('fill', '#fff') 84 | .style('fill-opacity', 0.2); 85 | 86 | this.mineText = this.mineG.append('text') 87 | .attr('class', 'detection-bar-text') 88 | .attr('x', barHeight / 2 + textMargin) 89 | .attr('y', barHeight / 2); 90 | 91 | } 92 | 93 | updateRequirements(probability, count) { 94 | 95 | this.requiredProbability = probability; 96 | this.requiredCount = count; 97 | 98 | this.percentRequired 99 | .attr('width', this.percentScale(probability)); 100 | 101 | this.counts = _.range(count); 102 | this.countBoxes = this.countG 103 | .selectAll('.count-box') 104 | .data(this.counts); 105 | 106 | this.countBoxes.enter() 107 | .append('rect') 108 | .attr('class', 'count-box detection-bar-box-stroke') 109 | .attr('x', (d, i) => barHeight * i) 110 | .attr('y', 0) 111 | .attr('width', barHeight / 2) 112 | .attr('height', barHeight) 113 | .style('fill', '#fff') 114 | .style('fill-opacity', 0.2); 115 | 116 | this.countBoxes.exit() 117 | .remove(); 118 | 119 | this.detectorsText 120 | .attr('x', barHeight * count - barHeight / 2 + textMargin); 121 | 122 | this.updateActual(0, 0, 0); 123 | } 124 | 125 | updateActual(probability, count, risk) { 126 | 127 | this.percentActual.transition().duration(absorptionDuration) 128 | .attr('width', this.percentScale(probability)); 129 | 130 | this.percentText 131 | .text(`${percentStr(probability)}% (out of ${percentStr(this.requiredProbability)}%) detection`); 132 | 133 | this.countBoxes.transition().duration(absorptionDuration) 134 | .style('fill', (d, i) => count > i ? '#0a0' : '#fff') 135 | .style('fill-opacity', (d, i) => count > i ? 1 : 0.2); 136 | 137 | this.mineBox.transition().duration(absorptionDuration) 138 | .style('fill', risk ? '#f00' : '#fff') 139 | .style('fill-opacity', risk ? 0.5 : 0.2); 140 | 141 | this.mineText 142 | .text(`${risk ? (100 * risk).toFixed(1) : ''}${risk ? '% risk' : "it's safe"}`) 143 | .classed('message-failure', risk); 144 | 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /js/drag_and_drop.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import {tileSize, repositionSpeed} from './config'; 3 | import {SoundService} from './sound_service'; 4 | import * as tile from './tile'; 5 | 6 | // TODO should also work without stock 7 | export const bindDrag = (tileSelection, board, stock) => { 8 | 9 | function reposition(data, keep = true) { 10 | delete data.newI; 11 | delete data.newJ; 12 | 13 | data.g 14 | .transition() 15 | .duration(repositionSpeed) 16 | .attr( 17 | 'transform', 18 | `translate(${data.x + tileSize / 2},${data.y + tileSize / 2})` 19 | ) 20 | .delay(repositionSpeed) 21 | .each((d) => { 22 | if (!keep) { 23 | d.g.remove(); 24 | } 25 | }); 26 | } 27 | 28 | const drag = d3.behavior.drag(); 29 | drag 30 | .on('dragstart', (source) => { 31 | 32 | d3.event.sourceEvent.stopPropagation(); 33 | source.top = false; 34 | 35 | if (board.animationExists) { 36 | board.stop(); 37 | board.callbacks.animationInterrupt(); 38 | } 39 | 40 | // Is it from stock? 41 | if (source.fromStock) { 42 | if (stock.stock[source.tileName] === 0) { 43 | source.dontDrag = true; 44 | SoundService.playThrottled('error'); 45 | return; 46 | } 47 | stock.regenerateTile(d3.select(source.node.parentNode)); 48 | stock.updateCount(source.tileName, -1); 49 | source.g.classed('stock-dragged', true); 50 | } 51 | 52 | // Is it impossible to drag item and it's not a Source? Play sound. 53 | if (source.frozen && source.tileName !== 'Source') { 54 | SoundService.playThrottled('error'); 55 | } 56 | }) 57 | .on('drag', function (source) { 58 | 59 | // Is it impossible to drag item? 60 | if (source.frozen) { 61 | return; 62 | } 63 | 64 | if (source.dontDrag) { 65 | return; 66 | } 67 | 68 | // Move element to the top 69 | if (!source.top) { 70 | // TODO still there are problems in Safari 71 | source.node.parentNode.appendChild(source.node); 72 | source.top = true; 73 | } 74 | 75 | d3.select(this) 76 | .attr('transform', `translate(${d3.event.x},${d3.event.y})`); 77 | source.newI = Math.floor(d3.event.x / tileSize); 78 | source.newJ = Math.floor(d3.event.y / tileSize); 79 | }) 80 | .on('dragend', (source) => { 81 | 82 | if (source.dontDrag) { 83 | delete source.dontDrag; 84 | return; 85 | } 86 | 87 | // No drag? Return. 88 | if (source.newI == null || source.newJ == null) { 89 | if (source.fromStock) { 90 | source.g.remove(); 91 | stock.updateCount(source.tileName, +1); 92 | } 93 | return; 94 | } 95 | 96 | // rotation fallback 97 | if (source.newI == source.i && source.newJ == source.j && !source.fromStock) { 98 | source.rotate(); 99 | SoundService.playThrottled('blip'); 100 | board.logger.logAction('rotate', {name: source.tileName, i: source.i, j: source.j, toRotation: source.rotation}); 101 | board.callbacks.tileRotated(source); 102 | // no return as I need to move it back to stick to the grid 103 | } 104 | 105 | // Drag ended outside of board? 106 | // The put in into the stock! 107 | if ( 108 | source.newI < 0 || source.newI >= board.level.width 109 | || source.newJ < 0 || source.newJ >= board.level.height 110 | ) { 111 | stock.updateCount(source.tileName, +1); 112 | board.logger.logAction('drag', { 113 | name: source.tileName, 114 | fromStock: !!source.fromStock, 115 | fromI: source.i, 116 | fromJ: source.j, 117 | toStock: true, 118 | success: !source.fromStock, 119 | }); 120 | if (source.fromStock) { 121 | reposition(source, false); 122 | } else { 123 | board.removeTile(source.i, source.j); 124 | } 125 | return; 126 | } 127 | 128 | // Otherwise... 129 | // Find target and target element 130 | const target = board.tileMatrix[source.newI][source.newJ]; 131 | 132 | // Dragged on an occupied tile? 133 | if (target.tileName !== 'Vacuum') { 134 | board.logger.logAction('drag', { 135 | name: source.tileName, 136 | fromStock: !!source.fromStock, 137 | fromI: source.i, 138 | fromJ: source.j, 139 | toStock: !!source.fromStock, 140 | toI: target.i, 141 | toJ: target.i, 142 | success: false, 143 | }); 144 | if (source.fromStock) { 145 | reposition(source, false); 146 | stock.updateCount(source.tileName, +1); 147 | } else { 148 | reposition(source, true); 149 | } 150 | return; 151 | } 152 | 153 | // Dragging on and empty tile 154 | if (!source.fromStock) { 155 | board.tileMatrix[source.i][source.j] = new tile.Tile(tile.Vacuum, 0, false, source.i, source.j); 156 | } 157 | board.logger.logAction('drag', { 158 | name: source.tileName, 159 | fromStock: !!source.fromStock, 160 | fromI: source.i, 161 | fromJ: source.j, 162 | toStock: false, 163 | toI: target.i, 164 | toJ: target.i, 165 | success: true, 166 | }); 167 | board.tileMatrix[target.i][target.j] = source; 168 | source.i = target.i; 169 | source.j = target.j; 170 | if (source.fromStock) { 171 | source.fromStock = false; 172 | board.boardGroup.node().appendChild(source.node); 173 | board.clickBehavior(source.g, board); 174 | source.g.insert('rect', ':first-child') 175 | .attr('class', (d) => d.frozen ? 'frost frost-frozen' : 'frost frost-nonfrozen') 176 | .attr('x', -tileSize / 2) 177 | .attr('y', -tileSize / 2) 178 | .attr('width', tileSize) 179 | .attr('height', tileSize); 180 | } 181 | reposition(source, true); 182 | 183 | }); 184 | 185 | tileSelection 186 | .call(drag); 187 | } 188 | -------------------------------------------------------------------------------- /js/game.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import _ from 'lodash'; 3 | import d3 from 'd3'; 4 | 5 | import * as level from './level'; 6 | import {GameBoard} from './game_board'; 7 | import {PopupManager} from './popup_manager'; 8 | import {SoundService} from './sound_service'; 9 | import {Storage} from './storage'; 10 | 11 | import {GameView} from './views/game_view'; 12 | import {LevelSelectorView} from './views/level_selector_view'; 13 | import {EncyclopediaSelectorView} from './views/encyclopedia_selector_view'; 14 | import {EncyclopediaItemView} from './views/encyclopedia_item_view'; 15 | 16 | export class Game { 17 | constructor() { 18 | // Initialize sound 19 | SoundService.initialize(); 20 | // Outer dependencies and controllers 21 | this.storage = new Storage(); 22 | // Pop-ups 23 | this.popupManager = new PopupManager( 24 | d3.select('.popup'), 25 | () => this.gameBoard.loadNextLevel()); 26 | // View definitions 27 | this.views = this.createViews(); 28 | // State 29 | this.gameBoard = null; 30 | this.currentEncyclopediaItem = null; 31 | } 32 | 33 | createViews() { 34 | return { 35 | levelSelector: new LevelSelectorView(this), 36 | game: new GameView(this), 37 | encyclopediaSelector: new EncyclopediaSelectorView(this), 38 | encyclopediaItem: new EncyclopediaItemView(this), 39 | } 40 | } 41 | 42 | setView(viewName) { 43 | if (!_.has(this.views, viewName)) { 44 | window.console.error(`Invalid view: ${viewName}`); 45 | return; 46 | } 47 | this.currentView = this.views[viewName]; 48 | // Set titles 49 | d3.select('.top-bar__title').text(this.currentView.title); 50 | // Switch visible content 51 | d3.selectAll(`.${this.currentView.className}`).classed('view--hidden', false); 52 | d3.selectAll(`.view:not(.${this.currentView.className})`).classed('view--hidden', true); 53 | } 54 | 55 | setEncyclopediaItem(item) { 56 | this.currentEncyclopediaItem = item; 57 | // Reset the encyclopedia item view 58 | this.views.encyclopediaItem.resetContent(); 59 | } 60 | 61 | htmlReady() { 62 | // Initialize views' controllers 63 | for (let view in this.views) { 64 | this.views[view].initialize(); 65 | } 66 | this.setView('game'); 67 | 68 | // for debugging purposes 69 | window.gameBoard = this.gameBoard; 70 | } 71 | 72 | createGameBoard() { 73 | const initialLevelId = this.storage.getCurrentLevelId() || level.levels[1].id; 74 | this.gameBoard = new GameBoard( 75 | d3.select('#game svg.game-svg'), 76 | d3.select('#game svg.blink-svg'), 77 | this, 78 | this.popupManager, 79 | this.storage, 80 | initialLevelId); 81 | } 82 | 83 | bindMenuEvents() { 84 | this.gameBoard.svg.select('.navigation-controls .level-list') 85 | .on('click', () => { 86 | this.gameBoard.stop(); 87 | this.setView('levelSelector'); 88 | }) 89 | .on('mouseover', () => 90 | this.gameBoard.titleManager.displayMessage('SELECT LEVEL') 91 | ); 92 | this.gameBoard.svg.select('.navigation-controls .encyclopedia') 93 | .on('click', () => { 94 | this.gameBoard.stop(); 95 | this.setView('encyclopediaSelector'); 96 | }) 97 | .on('mouseover', () => 98 | this.gameBoard.titleManager.displayMessage('ENCYCLOPEDIA') 99 | ); 100 | 101 | const overlay = this.gameBoard.svg.select('.interface-hint-overlay'); 102 | this.gameBoard.svg.select('.navigation-controls .help') 103 | .on('click', () => overlay.classed('hidden', !overlay.classed('hidden'))) 104 | .on('mouseover', () => overlay.classed('hidden', false)) 105 | .on('mouseout', () => overlay.classed('hidden', true)); 106 | 107 | this.gameBoard.svg.select('.navigation-controls .sandbox') 108 | .on('click', () => { 109 | this.gameBoard.loadLevel(level.levels[0].id); 110 | }) 111 | .on('mouseover', () => 112 | this.gameBoard.titleManager.displayMessage('SANDBOX LEVEL') 113 | ); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /js/game_board.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import d3 from 'd3'; 3 | import stringify from 'json-stringify-pretty-compact'; 4 | import {saveAs} from 'file-saver'; 5 | 6 | import {absorptionDuration, animationStepDurationMin, animationStepDurationMax, playPauseTransitionDuration, stockColumns, tileSize} from './config'; 7 | import {Stock} from './stock'; 8 | import * as level from './level'; 9 | import {BareBoard} from './bare_board'; 10 | import {ProgressPearls} from './progress_pearls'; 11 | import {TileHelper} from './tile_helper'; 12 | import {DetectionBar} from './detection_bar'; 13 | import {TitleManager} from './title_manager'; 14 | import {levelRecipe2queryString, queryString2levelRecipe} from './level_io_uri'; 15 | 16 | // TODO decide where to use winning status; it seems I should move it here 17 | // TODO top_bar needs a separate module 18 | 19 | export class GameBoard { 20 | constructor(svg, blinkSvg, game, popupManager, storage, levelId) { 21 | 22 | const borderMargins = { 23 | top: 2, 24 | left: 4, 25 | bottom: 2, 26 | right: 1 + stockColumns, 27 | }; 28 | this.bareBoard = new BareBoard(svg, this, 'orthogonal', 'Copenhagen', 29 | borderMargins, { 30 | tileRotated: this.tileRotatedCallback.bind(this), 31 | tileMouseover: this.tileMouseoverCallback.bind(this), 32 | animationStart: this.animationStartCallback.bind(this), 33 | animationInterrupt: this.animationInterruptCallback.bind(this), 34 | animationEnd: this.animationEndCallback.bind(this), 35 | setPlayButtonState: this.setPlayButtonState.bind(this), 36 | }); 37 | 38 | this.game = game; 39 | this.svg = svg; 40 | 41 | this.titleManager = new TitleManager( 42 | this.svg.select('.title-bar'), 43 | this.svg.select('.subtitle-bar'), 44 | blinkSvg 45 | ); 46 | this.titleManager.activateNextLevelButton(() => this.loadNextLevel()); 47 | 48 | this.popupManager = popupManager; 49 | this.storage = storage; 50 | 51 | this.progressPearls = new ProgressPearls( 52 | svg, 53 | level.levels.filter((d) => d.group === 'Game'), 54 | this 55 | ); 56 | this.progressPearls.g.attr('transform', `translate(${-1.8 * tileSize},${tileSize})`); 57 | this.progressPearls.draw(); 58 | 59 | this.stock = new Stock(svg, this.bareBoard); 60 | this.bareBoard.stock = this.stock; // such monkey patching not nice 61 | this.detectionBar = new DetectionBar(this.svg.select('.subtitle-bar')); 62 | this.detectionBar.g.attr('transform', `translate(${0.5 * tileSize},${tileSize / 4})`); 63 | this.logger = this.bareBoard.logger; 64 | this.logger.logAction('initialLevel'); 65 | 66 | this.boardControls = svg.selectAll('.board-controls'); 67 | this.activateBoardControls(); 68 | 69 | this.loadLevel(levelId); 70 | this.tileHelper = new TileHelper(svg, this.bareBoard, this.game); 71 | } 72 | 73 | tileRotatedCallback(tile) { 74 | this.showTileHelper(tile); 75 | } 76 | 77 | tileMouseoverCallback(tile) { 78 | this.showTileHelper(tile); 79 | } 80 | 81 | animationStartCallback() { 82 | this.saveProgress(); 83 | this.titleManager.displayMessage( 84 | 'Experiment in progress...', 85 | 'progress', -1); 86 | } 87 | 88 | animationInterruptCallback() { 89 | this.titleManager.displayMessage( 90 | 'Experiment disturbed! Quantum states are fragile...', 91 | 'failure'); 92 | // Reset play/pause button to "play" state 93 | this.setPlayButtonState('play'); 94 | } 95 | 96 | animationEndCallback() { 97 | 98 | const winningStatus = this.bareBoard.winningStatus; 99 | const level = this.bareBoard.level; 100 | 101 | // Reset play/pause button to "play" state 102 | this.setPlayButtonState('play'); 103 | 104 | this.detectionBar.updateActual( 105 | winningStatus.totalProbAtDets, 106 | winningStatus.noOfFedDets, 107 | winningStatus.noExplosion ? 0 : winningStatus.probsAtMines 108 | ); 109 | 110 | this.titleManager.displayMessage( 111 | winningStatus.message, 112 | winningStatus.isWon ? 'success' : 'failure', 113 | -1 114 | ); 115 | 116 | if (winningStatus.isWon) { 117 | 118 | if (!this.storage.getLevelIsWon(level.id)) { 119 | if (window.ga) { 120 | window.ga('send', 'event', 'Level', 'won', level.id); 121 | window.console.log('level winning logged'); 122 | } else { 123 | window.console.log('no Google Analytics to track winning'); 124 | } 125 | window.setTimeout( 126 | () => this.popupManager.popup('You won!', {close: true, nextLevel: true}), 127 | absorptionDuration 128 | ); 129 | } 130 | 131 | this.titleManager.showNextLevelButton(true); 132 | this.storage.setLevelIsWon(level.id, true); 133 | this.saveProgress(); 134 | this.progressPearls.update(); 135 | } 136 | } 137 | 138 | reset() { 139 | this.stop(); 140 | 141 | // Reset detection 142 | this.setHeaderTexts(); 143 | this.detectionBar.updateRequirements( 144 | this.bareBoard.level.requiredDetectionProbability, 145 | this.bareBoard.level.detectorsToFeed 146 | ); 147 | 148 | // Reset play/pause button to "play" state 149 | this.setPlayButtonState('play'); 150 | 151 | this.bareBoard.redraw(); 152 | // Hack: bareBoard SVG sets its viewBox - use that information to set 153 | // the viewBox of blinking SVG 154 | // TODO(pathes): more elegant mechanism 155 | this.titleManager.blinkSvg.attr('viewBox', this.svg.attr('viewBox')); 156 | this.stock.elementCount(this.bareBoard.level); 157 | this.stock.drawStock(); 158 | } 159 | 160 | stop() { 161 | this.bareBoard.stop(); 162 | } 163 | 164 | get level() { 165 | return this.bareBoard.level; 166 | // then also shortcut some gameBoard.level below 167 | } 168 | 169 | get title() { 170 | // const textBefore = (level) => 171 | // level.texts && level.texts.before ? `: "${level.texts.before}"` : ''; 172 | // // const groupPrefix = 173 | // // this.bareBoard.level.group ? 174 | // // `[${this.bareBoard.level.group}] ` : ''; 175 | // // return `${groupPrefix}${this.bareBoard.level.i}. ${this.bareBoard.level.name}${textBefore(this.bareBoard.level)}`; 176 | // return `${this.bareBoard.level.name}${textBefore(this.bareBoard.level)}`; 177 | return this.bareBoard.level.name; 178 | } 179 | 180 | get goalMessage() { 181 | if (this.bareBoard.level.requiredDetectionProbability === 0) { 182 | return 'GOAL: Avoid launching any mines!'; 183 | } else if (this.bareBoard.level.detectorsToFeed === 0) { 184 | return 'GOAL: No goals! Freedom to do whatever you like. :)'; 185 | } else if (this.bareBoard.level.detectorsToFeed === 1) { 186 | return `GOAL: Make the photon fall into a detector, with ${(100 * this.bareBoard.level.requiredDetectionProbability).toFixed(0)}% chance.`; 187 | } else { 188 | return `GOAL: Make the photon fall into ${this.bareBoard.level.detectorsToFeed} detectors, some probability to each, total of ${(100 * this.bareBoard.level.requiredDetectionProbability).toFixed(0)}%.`; 189 | } 190 | } 191 | 192 | get levelNumber() { 193 | return this.bareBoard.level.i; 194 | } 195 | 196 | setHeaderTexts() { 197 | this.titleManager.setTitle(this.title); 198 | this.titleManager.setDefaultMessage(this.goalMessage, ''); 199 | this.titleManager.setLevelNumber(this.levelNumber); 200 | } 201 | 202 | showTileHelper(tile) { 203 | 204 | this.tileHelper.show(tile); 205 | 206 | } 207 | 208 | /** 209 | * Set the play/pause button visual state. 210 | * @param newState string "play" or "pause" 211 | */ 212 | setPlayButtonState(newState) { 213 | if (newState !== 'play' && newState !== 'pause') { 214 | return; 215 | } 216 | const actualIcon = this.boardControls.select('.play .actual-icon'); 217 | const newStateIcon = d3.select(`#${newState}-icon`); 218 | actualIcon 219 | .transition() 220 | .duration(playPauseTransitionDuration) 221 | .attr('d', newStateIcon.attr('d')); 222 | } 223 | 224 | /** 225 | * Set up animation controls - bind events to buttons 226 | */ 227 | activateBoardControls() { 228 | // Don't let d3 bind clicked element as `this` to methods. 229 | const gameBoard = this; 230 | const bareBoard = this.bareBoard; 231 | const boardControls = this.boardControls; 232 | boardControls.select('.play') 233 | .on('click', bareBoard.play.bind(bareBoard)) 234 | .on('mouseover', () => gameBoard.titleManager.displayMessage('PLAY/PAUSE')); 235 | boardControls.select('.stop') 236 | .on('click', bareBoard.stop.bind(bareBoard)) 237 | .on('mouseover', () => gameBoard.titleManager.displayMessage('STOP')); 238 | boardControls.select('.forward') 239 | .on('click', bareBoard.forward.bind(bareBoard)) 240 | .on('mouseover', () => gameBoard.titleManager.displayMessage('NEXT STEP')); 241 | const durationToSlider = d3.scale.log() 242 | .domain([animationStepDurationMax, animationStepDurationMin]) 243 | .range([0, 1]); 244 | 245 | boardControls.select('.speed') 246 | .on('click', function () { 247 | const baseWidth = 100; // width in px in SVG without scaling 248 | const mouseX = d3.mouse(this)[0]; 249 | bareBoard.animationStepDuration = durationToSlider.invert(mouseX/baseWidth); 250 | gameBoard.titleManager.displayMessage( 251 | `Speed of light: ${(1000/bareBoard.animationStepDuration).toFixed(2)} tiles/s`, 252 | '' 253 | ); 254 | 255 | d3.select(this).select('rect') 256 | .attr('x', mouseX - 3); 257 | }) 258 | .on('mouseover', () => gameBoard.titleManager.displayMessage('CHANGE SPEED')); 259 | 260 | boardControls.select('.reset') 261 | .on('click', () => { 262 | gameBoard.reloadLevel(false); 263 | }) 264 | .on('mouseover', () => gameBoard.titleManager.displayMessage('RESET LEVEL')); 265 | 266 | boardControls.select('.download') 267 | .on('click', () => { 268 | bareBoard.logger.logAction('download'); 269 | gameBoard.downloadCurrentLevel(); 270 | }) 271 | .on('mouseover', () => gameBoard.titleManager.displayMessage('DOWNLOAD LEVEL AS JSON')); 272 | 273 | boardControls.select('.view-mode') 274 | .on('click', function () { 275 | let newMode; 276 | if (bareBoard.drawMode === 'oscilloscope') { 277 | newMode = 'orthogonal'; 278 | } else { 279 | newMode = 'oscilloscope'; 280 | } 281 | bareBoard.drawMode = newMode; 282 | d3.select(this) 283 | .select('text') 284 | .html(newMode); 285 | }); 286 | 287 | boardControls.select('.measurement-mode') 288 | .on('click', function () { 289 | let newMode; 290 | if (bareBoard.measurementMode === 'Copenhagen') { 291 | newMode = 'delayed meas.'; 292 | } else { 293 | newMode = 'Copenhagen'; 294 | } 295 | bareBoard.measurementMode = newMode; 296 | d3.select(this) 297 | .select('text') 298 | .html(newMode); 299 | }); 300 | } 301 | 302 | downloadCurrentLevel() { 303 | const levelJSON = stringify(this.bareBoard.exportBoard(), {maxLength: 100, indent: 2}); 304 | const fileName = _.kebabCase(`${this.bareBoard.level.name}_${(new Date()).toISOString()}`) + '.json'; 305 | const blob = new Blob([levelJSON], {type: 'text/plain;charset=utf-8'}); 306 | saveAs(blob, fileName); 307 | window.console.log(levelJSON); 308 | 309 | // now for testing 310 | window.console.log( 311 | 'levelRecipe2queryString(this.bareBoard.exportBoard())', 312 | levelRecipe2queryString(this.bareBoard.exportBoard()) 313 | ); 314 | 315 | window.console.log( 316 | 'queryString2levelRecipe(levelRecipe2queryString(this.bareBoard.exportBoard()))', 317 | queryString2levelRecipe(levelRecipe2queryString(this.bareBoard.exportBoard())) 318 | ); 319 | } 320 | 321 | 322 | loadLevel(levelId, checkStorage = true, dev = false) { 323 | 324 | this.saveProgress(); 325 | this.logger.save(); 326 | this.logger.reset(); 327 | 328 | let levelToLoad = null; 329 | let loadedFromStorage = false; 330 | 331 | // Try to load level from storage 332 | if (checkStorage && this.storage.hasLevelProgress(levelId)) { 333 | levelToLoad = this.storage.getLevelProgress(levelId); 334 | this.logger.logAction('loadLevel', {fromStorage: true}); 335 | loadedFromStorage = true; 336 | } 337 | 338 | // Try to create level from scratch, if such exists 339 | if (!loadedFromStorage && level.idToLevel[levelId] != null) { 340 | levelToLoad = level.idToLevel[levelId]; 341 | this.logger.logAction('loadLevel', {fromStorage: false}); 342 | } 343 | 344 | // If levelId is invalid, load first Level 345 | if (levelToLoad == null) { 346 | // TODO(pathes): remove magic constant 347 | levelToLoad = level.levels[1]; 348 | // NOTE(migdal): it is an ugly piece which already made me waste some time 349 | // ideally - exception; at very least - console.log 350 | window.console.log(`XXX For levelId ${levelId} there is no level; falling back to the first level.`); 351 | this.logger.logAction('invalidLoadLevel', {}); 352 | } 353 | 354 | // Additionally, check if level is passed. If not, show popup. 355 | if (!this.storage.getLevelIsWon(levelToLoad.id) && levelToLoad.initialHint != null) { 356 | this.popupManager.popup(levelToLoad.initialHint, {close: true, nextLevel: false}); 357 | } 358 | 359 | this.storage.setCurrentLevelId(levelId); 360 | this.bareBoard.level = new level.Level(levelToLoad, dev ? 'dev' : 'game'); 361 | this.bareBoard.alreadyWon = this.storage.getLevelIsWon(levelId); 362 | this.reset(); 363 | this.progressPearls.update(); 364 | 365 | this.titleManager.showNextLevelButton(this.bareBoard.alreadyWon); 366 | 367 | } 368 | 369 | loadNextLevel() { 370 | if (this.bareBoard.level && this.bareBoard.level.next) { 371 | this.loadLevel(this.bareBoard.level.next); 372 | } 373 | } 374 | 375 | // dev = true only from console 376 | reloadLevel(dev = false) { 377 | this.loadLevel(this.bareBoard.level.id, false, dev); 378 | } 379 | 380 | saveProgress() { 381 | // Save progress if there was any level loaded 382 | if (this.bareBoard.level != null) { 383 | this.storage.setLevelProgress(this.bareBoard.level.id, this.bareBoard.exportBoard()); 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /js/level.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {nonVacuumTiles} from './tile'; 4 | import {isProduction} from './config'; 5 | 6 | import levelsGame from '../data/levels_game.json!'; 7 | import levelsCandidate from '../data/levels_candidate.json!'; 8 | import levelsOther from '../data/levels_other.json!'; 9 | import lastLevel from '../data/levels_last.json!'; 10 | 11 | 12 | export class Level { 13 | constructor(levelRecipe, mode = 'game') { 14 | // TODO(migdal) remove mindless attribute copying 15 | // It cannot be done using _.assign(this, _.pick(levelRecipe, [...])), 16 | // because Level is not exactly an Object instance. 17 | this.next = levelRecipe.next; 18 | this.name = levelRecipe.name; 19 | if (mode === 'dev') { 20 | this.group = 'A Dev'; 21 | } else { 22 | this.group = levelRecipe.group; 23 | } 24 | this.i = levelRecipe.i; 25 | this.id = levelRecipe.id; 26 | this.next = levelRecipe.next; 27 | this.width = levelRecipe.width; 28 | this.height = levelRecipe.height; 29 | this.initialHint = levelRecipe.initialHint; 30 | this.boardHints = levelRecipe.boardHints || []; 31 | this.texts = levelRecipe.texts || {}; 32 | this.tileRecipes = levelRecipe.tiles; 33 | this.initialStock = {}; 34 | if (levelRecipe.stock == null && _.filter(levelRecipe.tiles, 'frozen').length === 0) { 35 | levelRecipe.stock = 'all'; 36 | } 37 | if (typeof levelRecipe.stock === 'object' || mode === 'as_it_is') { 38 | this.initialStock = levelRecipe.stock || {}; 39 | } else if (levelRecipe.stock === 'all' || mode === 'dev') { 40 | nonVacuumTiles.forEach((tile) => { 41 | this.initialStock[tile] = (tile === 'Source' ? 1 : 99); 42 | }); 43 | } else if (levelRecipe.stock === 'non-frozen' || mode === 'game') { 44 | this.tileRecipes = _.filter(levelRecipe.tiles, 'frozen'); 45 | this.initialStock = _(levelRecipe.tiles) 46 | .filter((tile) => !tile.frozen) 47 | .countBy('name') 48 | .value(); 49 | } 50 | this.requiredDetectionProbability = levelRecipe.requiredDetectionProbability === undefined ? 1 : levelRecipe.requiredDetectionProbability; 51 | this.detectorsToFeed = levelRecipe.detectorsToFeed || _.filter(levelRecipe.tiles, (tile) => tile.frozen && (tile.name === 'Detector' || tile.name === 'DetectorFour')).length; 52 | } 53 | } 54 | 55 | const levelId = (level) => `${level.group} ${level.name}`; 56 | 57 | if (!isProduction) { 58 | levelsCandidate.forEach((level) => level.group = 'Game'); 59 | } else { 60 | levelsCandidate.forEach((level) => level.group = 'X Candidate'); 61 | } 62 | 63 | export const levels = _(levelsGame) 64 | .concat(levelsCandidate) 65 | .concat(levelsOther) 66 | .map((level, i) => { 67 | level.i = i; 68 | level.id = levelId(level); 69 | return level; 70 | }) 71 | .sortBy((level) => `${level.group} ${1e6 + level.i}`) 72 | .value(); 73 | 74 | if (isProduction) { 75 | lastLevel.i = -1; 76 | lastLevel.group = 'Special'; 77 | lastLevel.id = '3413472342'; 78 | levels.push(lastLevel); 79 | } 80 | 81 | levels.forEach((level, i) => { 82 | level.next = _.get(levels[i + 1], 'id'); 83 | delete level.i; 84 | }); 85 | 86 | // ordering within groups 87 | _(levels) 88 | .groupBy('group') 89 | .forEach((group) => 90 | group.forEach((level, i) => level.i = i + 1) 91 | ); 92 | 93 | levels[0].i = '\u221E'; 94 | 95 | export const idToLevel = _.keyBy(levels, 'id'); 96 | -------------------------------------------------------------------------------- /js/level.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {levels, Level} from './level'; 4 | 5 | describe('All level JSON recipes have required fields', () => { 6 | levels.forEach((levelRecipe) => { 7 | it(`${levelRecipe.group} ${levelRecipe.i} ${levelRecipe.name}`, () => { 8 | expect(levelRecipe.group).toBeDefined(); 9 | expect(levelRecipe.name).toBeDefined(); 10 | expect(levelRecipe.id).toBeDefined(); 11 | expect(levelRecipe.width).toBeDefined(); 12 | expect(levelRecipe.height).toBeDefined(); 13 | expect(levelRecipe.tiles).toBeDefined(); 14 | }); 15 | }) 16 | }); 17 | 18 | describe('Levels are present', () => { 19 | 20 | it('At least 1 dev level', () => { 21 | 22 | expect( 23 | levels.filter((levelRecipe) => 24 | levelRecipe.tiles.length === 0 && levelRecipe.stock === 'all' 25 | ).length 26 | ).toBeGreaterThan(0); 27 | 28 | }); 29 | 30 | it('At least 10 game levels', () => { 31 | expect(levels.filter((levelRecipe) => levelRecipe.group === 'Game').length).toBeGreaterThan(9); 32 | }); 33 | 34 | }); 35 | 36 | describe('Game levels: source, detector, mines - present, fixed', () => { 37 | 38 | levels 39 | .filter((levelRecipe) => levelRecipe.group === 'Game') 40 | .forEach((levelRecipe) => { 41 | it(`${levelRecipe.i} ${levelRecipe.name}`, () => { 42 | 43 | const tileCount = _.countBy(levelRecipe.tiles, 'name'); 44 | 45 | expect(tileCount['Source']).toBe(1); 46 | expect((tileCount['Detector'] || 0) + (tileCount['Mine'] || 0)).toBeGreaterThan(0); 47 | 48 | const nonfrozenCount = _(levelRecipe.tiles) 49 | .filter((tile) => !tile.frozen) 50 | .countBy('name') 51 | .value(); 52 | 53 | expect(nonfrozenCount['Source']).toBeUndefined(); 54 | expect(nonfrozenCount['Detector']).toBeUndefined(); 55 | expect(nonfrozenCount['Mine']).toBeUndefined(); 56 | 57 | }); 58 | }) 59 | }); 60 | 61 | describe('Level group-name pairs are unique', () => { 62 | 63 | it(`${levels.length} level names are unique`, () => { 64 | const uniqueLength = _(levels) 65 | .map((levelRecipe) => `${levelRecipe.group} ${levelRecipe.name}`) 66 | .uniq() 67 | .value() 68 | .length; 69 | expect(uniqueLength).toBe(levels.length); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /js/level_io_uri.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | // NOTE could be done automatically, but mnemotechnics may make sense 4 | const tileAbbreviations = [ 5 | ['Vacuum', 'u'], 6 | ['Source', 's'], 7 | ['CornerCube', 'x'], 8 | ['ThinMirror', 't'], 9 | ['ThinSplitter', 'h'], 10 | ['ThinSplitterCoated', 'c'], 11 | ['PolarizingSplitter', 'b'], 12 | ['PolarizerNS', 'p'], 13 | ['PolarizerWE', 'l'], 14 | ['QuarterWavePlateNS', 'q'], 15 | ['QuarterWavePlateWE', 'w'], 16 | ['SugarSolution', 'g'], 17 | ['DoubleSugarSolution', 'i'], 18 | ['Mine', 'm'], 19 | ['Rock', 'k'], 20 | ['Glass', 'a'], 21 | ['VacuumJar', 'v'], 22 | ['Absorber', 'o'], 23 | ['Detector', 'd'], 24 | ['DetectorFour', 'e'], 25 | ['FaradayRotator', 'f'], 26 | ]; 27 | 28 | // export only for tests 29 | export const name2abbr = _.fromPairs(tileAbbreviations); 30 | const abbr2name = _(tileAbbreviations) 31 | .map((each) => [each[1], each[0]]) 32 | .fromPairs() 33 | .value(); 34 | 35 | const vacuumCode = name2abbr['Vacuum'] + '0'; 36 | 37 | // e.g. {name: 'Source', frozen: true, rotation: 2} -> 'S2' 38 | export const encodeTile = (tileRecipe) => { 39 | let s = name2abbr[tileRecipe.name]; 40 | if (tileRecipe.frozen) { 41 | s = s.toUpperCase(); 42 | } 43 | return `${s}${tileRecipe.rotation.toFixed(0)}`; 44 | } 45 | 46 | // e.g. 'S2' -> {name: 'Source', frozen: true, rotation: 2} 47 | export const decodeTile = (abbrRot) => ({ 48 | name: abbr2name[abbrRot[0].toLowerCase()], 49 | frozen: abbrRot[0] === abbrRot[0].toUpperCase(), 50 | rotation: parseInt(abbrRot[1]), 51 | }); 52 | 53 | const encodeKeyValue = (k, v) => 54 | `${k}=${window.encodeURIComponent(v)}`; 55 | 56 | const serializeAllTiles = (tiles, width, height) => { 57 | const tileMatrix = _.range(height).map(() => 58 | _.range(width).map(() => vacuumCode) 59 | ); 60 | tiles.forEach((tileRecipe) => { 61 | tileMatrix[tileRecipe.j][tileRecipe.i] = encodeTile(tileRecipe); 62 | }); 63 | return _(tileMatrix).flatten().join(''); 64 | }; 65 | 66 | export const levelRecipe2queryString = (levelRecipe) => 67 | [ 68 | ['n', levelRecipe.name], 69 | ['w', levelRecipe.width], 70 | ['h', levelRecipe.height], 71 | ['t', serializeAllTiles(levelRecipe.tiles, levelRecipe.width, levelRecipe.height)], 72 | // ['s', ...] for now without stock 73 | ] 74 | .map((each) => encodeKeyValue(each[0], each[1])) 75 | .join('&'); 76 | 77 | // for one-letter keys 78 | const parseQueryString = (queryString) => 79 | _(queryString.split('&')) 80 | .map((s) => [s[0], decodeURIComponent(s.slice(2))]) 81 | .fromPairs() 82 | .value(); 83 | 84 | const parseAllTiles = (allTileString, width) => 85 | _.range(allTileString.length / 2) 86 | .map((k) => ({ 87 | i: k % width, 88 | j: Math.floor(k / width), 89 | t: allTileString.slice(2 * k, 2 * k + 2), 90 | })) 91 | .filter((tile) => tile.t !== vacuumCode) 92 | .map((tile) => { 93 | const res = decodeTile(tile.t); 94 | res.i = tile.i; 95 | res.j = tile.j; 96 | return res; 97 | }); 98 | 99 | export const queryString2levelRecipe = (queryString) => { 100 | const params = parseQueryString(queryString); 101 | return { 102 | name: params.n, 103 | group: 'Shared', 104 | id: -1, // maybe some hash? 105 | i: -1, // no idea 106 | next: null, 107 | width: parseInt(params.w), 108 | height: parseInt(params.h), 109 | tiles: parseAllTiles(params.t, params.w), 110 | }; 111 | } 112 | 113 | // Q: 114 | // should I attach key? or version 115 | // as I will add new elements 116 | -------------------------------------------------------------------------------- /js/level_io_uri.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {name2abbr, encodeTile, decodeTile} from './level_io_uri'; 4 | import {allTiles} from './tile'; 5 | 6 | 7 | describe('Tile URI codes', () => { 8 | 9 | const noOfCodes = _.values(name2abbr).length; 10 | 11 | it('Tile codes are unique', () => { 12 | const uniqueLength = _(name2abbr) 13 | .values() 14 | .uniq() 15 | .value() 16 | .length; 17 | expect(uniqueLength).toBe(noOfCodes); 18 | }); 19 | 20 | it('As many codes as tiles', () => { 21 | expect(noOfCodes).toBe(allTiles.length); 22 | }); 23 | 24 | it('Each tile has its code', () => { 25 | const numberOfTilesWithCode = _(allTiles) 26 | .map((name) => _.has(name2abbr, name)) 27 | .sum(); 28 | expect(numberOfTilesWithCode).toBe(allTiles.length); 29 | }); 30 | 31 | }); 32 | 33 | 34 | describe('Encoding and decoding tile URI codes', () => { 35 | 36 | it('Decoding and encoding rotated ThinSplitter: h3 and H3', () => { 37 | expect(encodeTile(decodeTile('h3'))).toBe('h3'); 38 | expect(encodeTile(decodeTile('H3'))).toBe('H3'); 39 | }); 40 | 41 | }); 42 | 43 | 44 | // TODO(migdal) encoding and decoding whole board 45 | -------------------------------------------------------------------------------- /js/logger.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | // level-level logger 4 | // TODO also a general level logger 5 | 6 | // NSA approves! 7 | // PiS tez! 8 | 9 | export class Logger { 10 | 11 | constructor(databaseConnector) { 12 | this.reset(); 13 | this.logAction('loggingStarted', {clientAbsTime: (new Date()).toISOString()}); 14 | } 15 | 16 | logAction(actionName, dict = {}) { 17 | this.log.push([actionName, +(new Date()) - this.time0, dict]); 18 | } 19 | 20 | reset() { 21 | this.log = []; 22 | this.time0 = +(new Date()); 23 | } 24 | 25 | save() { 26 | // save to DB 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /js/particle/canvas_particle_animation.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import _ from 'lodash'; 3 | import d3 from 'd3'; 4 | 5 | import {TAU, perpendicularI, perpendicularJ} from '../const'; 6 | import {tileSize, oscillations, polarizationScaleH, polarizationScaleV, resizeThrottle, canvasDrawFrequency} from '../config'; 7 | import {ParticleAnimation} from './particle_animation'; 8 | 9 | export class CanvasParticleAnimation extends ParticleAnimation { 10 | constructor(board, history, measurementHistory, absorptionProbabilities, interruptCallback, finishCallback, drawMode, displayMessage) { 11 | super(board, history, measurementHistory, absorptionProbabilities, interruptCallback, finishCallback, drawMode, displayMessage); 12 | this.canvas = null; 13 | this.helperCanvas = null; 14 | this.ctx = null; 15 | this.startTime = 0; 16 | this.pauseTime = 0; 17 | // Prepare throttled version of resizeCanvas 18 | this.throttledResizeCanvas = 19 | _.throttle(this.resizeCanvas, resizeThrottle) 20 | .bind(this); 21 | } 22 | 23 | updateStartTime() { 24 | // If we paused, we have to change startTime for animation to work. 25 | if (!this.playing && this.startTime <= this.pauseTime) { 26 | const time = new Date().getTime(); 27 | this.startTime += time - this.pauseTime; 28 | } 29 | } 30 | 31 | stop() { 32 | super.stop(); 33 | window.removeEventListener('resize', this.throttledResizeCanvas); 34 | this.canvas.classed('canvas--hidden', true); 35 | } 36 | 37 | play() { 38 | this.updateStartTime(); 39 | super.play(); 40 | this.canvas.classed('canvas--hidden', false); 41 | } 42 | 43 | forward() { 44 | this.updateStartTime(); 45 | super.forward(); 46 | } 47 | 48 | initialize() { 49 | super.initialize(); 50 | // Create canvas, get context 51 | this.canvas = d3.select('#gameCanvas'); 52 | this.ctx = this.canvas[0][0].getContext('2d'); 53 | // Similar for helper canvas 54 | this.helperCanvas = d3.select('#gameHelperCanvas'); 55 | this.helperCtx = this.helperCanvas[0][0].getContext('2d'); 56 | // Interrupt animation when clicked on canvas 57 | this.canvas[0][0].addEventListener('click', this.interrupt.bind(this)); 58 | // Initial canvas resize 59 | this.resizeCanvas(); 60 | // Cancel old clearing events 61 | CanvasParticleAnimation.stopClearing(); 62 | // Set resize event handler 63 | window.addEventListener('resize', this.throttledResizeCanvas); 64 | this.startTime = new Date().getTime(); 65 | this.lastStepFloat = 0; 66 | // Show the canvas (useful when initing animation via "next step" button) 67 | this.canvas.classed('canvas--hidden', false); 68 | } 69 | 70 | interrupt() { 71 | this.stop(); 72 | this.interruptCallback(); 73 | } 74 | 75 | resizeCanvas() { 76 | // Get the size of #game > svg > .background element 77 | const box = this.board.svg.select('.background').node().getBoundingClientRect(); 78 | const resizer = (canvas) => { 79 | canvas 80 | .style({ 81 | width: `${Math.round(box.width)}px`, 82 | height: `${Math.round(box.height)}px`, 83 | top: `${Math.round(box.top)}px`, 84 | left: `${Math.round(box.left)}px`, 85 | }) 86 | .attr({ 87 | width: this.board.level.width * tileSize, 88 | height: this.board.level.height * tileSize, 89 | }); 90 | } 91 | resizer(this.canvas); 92 | resizer(this.helperCanvas); 93 | } 94 | 95 | nextFrame() { 96 | const time = new Date().getTime(); 97 | const stepFloat = (time - this.startTime) / this.animationStepDuration; 98 | const oldStepNo = this.stepNo; 99 | this.stepNo = Math.floor(stepFloat); 100 | const stepIncreased = this.stepNo > oldStepNo; 101 | 102 | if (this.stepNo < this.history.length - 1) { 103 | const relativeStepNo = stepFloat - this.stepNo; 104 | const relativeLastStepNo = this.lastStepFloat - this.stepNo; 105 | this.updateParticles(relativeStepNo, relativeLastStepNo); 106 | if (stepIncreased) { 107 | this.displayMeasurementTexts(this.stepNo); 108 | } 109 | // Update last step 110 | this.lastStepFloat = stepFloat; 111 | // Request next frame if playing or if the animation didn't manage 112 | // to get to the keyframe. 113 | if (this.playing || !stepIncreased) { 114 | window.requestAnimationFrame(this.forward.bind(this)); 115 | } else { 116 | this.pauseTime = time; 117 | } 118 | } else { 119 | this.finish(); 120 | } 121 | } 122 | 123 | /** 124 | * 125 | */ 126 | updateParticles(stepFloat, lastStepFloat) { 127 | const substepStart = Math.round(lastStepFloat * canvasDrawFrequency); 128 | const substepEnd = Math.round(stepFloat * canvasDrawFrequency); 129 | for (let substep = substepStart; substep <= substepEnd; ++substep) { 130 | const t = substep / canvasDrawFrequency; 131 | if (this.drawMode === 'oscilloscope') { 132 | this.drawParticlesOscilloscopeMode(t); 133 | } else { 134 | this.drawParticlesOrthogonalMode(t); 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Draw particles basing on current step (this.stepNo) 141 | * and t, which represents how far we are in progression 142 | * from step stepNo to step (stepNo + 1). 143 | * 144 | * `t` usually should be in range [0, 1). 145 | * It may happen that it's below 0, e.g. when we draw particles from previous 146 | * frame. 147 | */ 148 | drawParticlesOrthogonalMode(t) { 149 | this.clearAlpha(0.95); 150 | // Determine which step to access. It is possible that we progressed with 151 | // this.stepNo, but we have still to draw some dots from previous step. 152 | let stepNo = this.stepNo; 153 | while (t < 0) { 154 | stepNo--; 155 | t += 1; 156 | } 157 | // Actual drawing 158 | this.ctx.fillStyle = 'red'; 159 | _.each(this.history[stepNo], (d) => { 160 | this.ctx.beginPath(); 161 | this.ctx.globalAlpha = d.prob; 162 | const h = polarizationScaleH * (d.hRe * Math.cos(oscillations * TAU * t) + d.hIm * Math.sin(oscillations * TAU * t)) / Math.sqrt(d.prob); 163 | const x = (1 - t) * d.startX + t * d.endX + perpendicularI[d.dir] * h; 164 | const y = (1 - t) * d.startY + t * d.endY + perpendicularJ[d.dir] * h; 165 | const s = 10 * ( 166 | 1 + polarizationScaleV * ( 167 | d.vRe * Math.cos(oscillations * TAU * t) 168 | + d.vIm * Math.sin(oscillations * TAU * t) 169 | ) / Math.sqrt(d.prob) 170 | ); 171 | this.ctx.arc(x, y, s, 0, 360, false); 172 | this.ctx.fill(); 173 | }); 174 | } 175 | 176 | drawParticlesOscilloscopeMode(t) { 177 | this.clearAlpha(0.9); 178 | // Determine which step to access. It is possible that we progressed with 179 | // this.stepNo, but we have still to draw some dots from previous step. 180 | let stepNo = this.stepNo; 181 | while (t < 0) { 182 | stepNo--; 183 | t += 1; 184 | } 185 | // Actual drawing 186 | this.ctx.fillStyle = 'red'; 187 | _.each(this.history[stepNo], (d) => { 188 | 189 | const movX = (1 - t) * d.startX + t * d.endX; 190 | const movY = (1 - t) * d.startY + t * d.endY; 191 | 192 | const polH = 25 * (d.hRe * Math.cos(oscillations * TAU * t) + d.hIm * Math.sin(oscillations * TAU * t)); 193 | const polV = 25 * (d.vRe * Math.cos(oscillations * TAU * t) + d.vIm * Math.sin(oscillations * TAU * t)); 194 | 195 | const polX = perpendicularI[d.dir] * polV + perpendicularJ[d.dir] * polH; 196 | const polY = perpendicularI[d.dir] * polH + perpendicularJ[d.dir] * polV; 197 | 198 | const x = movX + polX; 199 | const y = movY + polY; 200 | 201 | this.ctx.beginPath(); 202 | this.ctx.globalAlpha = d.prob * 0.5; 203 | this.ctx.strokeStyle = 'orange'; 204 | this.ctx.arc(movX, movY, 45, 0, 360, false); 205 | this.ctx.stroke(); 206 | 207 | this.ctx.beginPath(); 208 | this.ctx.globalAlpha = d.prob; 209 | this.ctx.fillStyle = 'red'; 210 | this.ctx.arc(x, y, 5, 0, 360, false); 211 | this.ctx.fill(); 212 | }); 213 | } 214 | 215 | finish() { 216 | super.finish(); 217 | this.startClearing(); 218 | } 219 | 220 | startClearing() { 221 | // There may be multiple existing instances of CanvasParticleAnimation 222 | // at the same time - if player presses `play` just after previous animation 223 | // has ended. There may be an overlap between old animation clearing 224 | // and new animation. 225 | // Global counter allowing one animation to cancel clearing of the other one. 226 | CanvasParticleAnimation.clearingFramesLeft = 20; 227 | this.clearing(); 228 | } 229 | 230 | clearing() { 231 | if ( 232 | CanvasParticleAnimation.clearingFramesLeft == null 233 | || CanvasParticleAnimation.clearingFramesLeft <= 0 234 | ) { 235 | return; 236 | } 237 | if (CanvasParticleAnimation.clearingFramesLeft === 1) { 238 | this.clearAlpha(0); 239 | this.canvas.classed('canvas--hidden', true); 240 | return; 241 | } 242 | CanvasParticleAnimation.clearingFramesLeft--; 243 | this.clearAlpha(0.8); 244 | window.setTimeout(this.clearing.bind(this), 50); 245 | } 246 | 247 | static stopClearing() { 248 | CanvasParticleAnimation.clearingFramesLeft = 0; 249 | } 250 | 251 | /** 252 | * clearRect with alpha support. 253 | * alpha - how much (in terms of transparency) of previous frame stays. 254 | * clearAlpha(0) should work like clearRect(). 255 | */ 256 | clearAlpha(alpha) { 257 | // Reset alpha 258 | this.ctx.globalAlpha = 1; 259 | // Copy image to helper context 260 | if (alpha > 0) { 261 | this.helperCtx.clearRect( 262 | 0, 0, 263 | this.board.level.width * tileSize, 264 | this.board.level.height * tileSize 265 | ); 266 | this.helperCtx.globalAlpha = alpha; 267 | this.helperCtx.drawImage(this.canvas[0][0], 0, 0); 268 | } 269 | // Draw image from helper context, a bit faded-out 270 | this.ctx.clearRect( 271 | 0, 0, 272 | this.board.level.width * tileSize, 273 | this.board.level.height * tileSize 274 | ); 275 | if (alpha > 0) { 276 | this.ctx.drawImage(this.helperCanvas[0][0], 0, 0); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /js/particle/particle.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import {velocityI, velocityJ} from '../const'; 3 | import {tileSize} from '../config'; 4 | 5 | export class Particle { 6 | 7 | constructor(i, j, dir, hRe, hIm, vRe, vIm) { 8 | this.i = i; 9 | this.j = j; 10 | this.dir = dir; 11 | this.hRe = hRe; 12 | this.hIm = hIm; 13 | this.vRe = vRe; 14 | this.vIm = vIm; 15 | } 16 | 17 | get startX() { 18 | return tileSize * this.i + tileSize / 2; 19 | } 20 | 21 | get endX() { 22 | return tileSize * (this.i + velocityI[this.dir]) + tileSize / 2; 23 | } 24 | 25 | get startY() { 26 | return tileSize * this.j + tileSize / 2; 27 | } 28 | 29 | get endY() { 30 | return tileSize * (this.j + velocityJ[this.dir]) + tileSize / 2; 31 | } 32 | 33 | get prob() { 34 | return this.hRe * this.hRe + this.hIm * this.hIm 35 | + this.vRe * this.vRe + this.vIm * this.vIm; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /js/particle/particle_animation.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import _ from 'lodash'; 3 | 4 | import {tileSize, absorptionDuration, absorptionTextDuration} from '../config'; 5 | import {Particle} from './particle'; 6 | import * as print from '../print'; 7 | 8 | export class ParticleAnimation { 9 | constructor(board, history, measurementHistory, absorptionProbabilities, interruptCallback, finishCallback, drawMode, displayMessage) { 10 | 11 | this.stateHistory = history; 12 | this.history = history.map((state) => { 13 | return _.chain(state) 14 | .groupBy((val) => `${val.i},${val.j},${val.to[0]}`) 15 | .mapValues((ray) => { 16 | const rayind = _.keyBy(ray, (val) => val.to[1]); 17 | 18 | const hRe = rayind['-'] ? rayind['-'].re : 0; 19 | const hIm = rayind['-'] ? rayind['-'].im : 0; 20 | const vRe = rayind['|'] ? rayind['|'].re : 0; 21 | const vIm = rayind['|'] ? rayind['|'].im : 0; 22 | 23 | return new Particle(ray[0].i, ray[0].j, ray[0].to[0], hRe, hIm, vRe, vIm); 24 | }) 25 | .values() 26 | .value(); 27 | }); 28 | 29 | this.measurementHistory = measurementHistory; 30 | this.absorptionProbabilities = absorptionProbabilities; 31 | this.animationStepDuration = board.animationStepDuration; 32 | this.interruptCallback = interruptCallback; 33 | this.finishCallback = finishCallback; 34 | this.drawMode = drawMode; 35 | this.board = board; 36 | this.displayMessage = displayMessage; 37 | this.stepNo = 0; 38 | this.playing = false; 39 | this.initialized = false; 40 | // report it to the board 41 | this.board.animationExists = true; 42 | 43 | this.previousStepNo = -1; 44 | } 45 | 46 | initialize() { 47 | this.measurementTextGroup = this.board.svg 48 | .append('g') 49 | .attr('class', 'measurement-texts'); 50 | this.absorptionTextGroup = this.board.svg 51 | .append('g') 52 | .attr('class', 'absorption-texts'); 53 | this.initialized = true; 54 | this.board.animationExists = true; 55 | } 56 | 57 | play() { 58 | if (!this.initialized) { 59 | this.initialize(); 60 | } 61 | if (!this.playing) { 62 | this.playing = true; 63 | this.forward(); 64 | } 65 | } 66 | 67 | stop() { 68 | this.pause(); 69 | this.removeTexts(); 70 | this.initialized = false; 71 | this.board.animationExists = false; 72 | } 73 | 74 | pause() { 75 | this.playing = false; 76 | } 77 | 78 | forward() { 79 | if (this.stepNo > this.previousStepNo) { 80 | this.previousStepNo = this.stepNo; 81 | this.displayMessage(print.stateToStr(this.stateHistory[this.stepNo])); 82 | } 83 | this.nextFrame(); 84 | } 85 | 86 | nextFrame() { 87 | throw new Error('nextFrame() unimplemented'); 88 | } 89 | 90 | removeTexts() { 91 | this.measurementTextGroup.remove(); 92 | this.absorptionTextGroup.remove(); 93 | } 94 | 95 | // NOTE maybe just one timeout would suffice 96 | finish() { 97 | window.setTimeout( 98 | this.displayAbsorptionTexts.bind(this), 99 | absorptionDuration 100 | ); 101 | const lastStep = this.measurementHistory.length - 1; 102 | window.setTimeout( 103 | this.displayMeasurementTexts.bind(this, lastStep), 104 | this.animationStepDuration 105 | ); 106 | window.setTimeout( 107 | this.finishCallback.bind(this), 108 | this.absorptionDuration 109 | ); 110 | window.setTimeout( 111 | () => {this.board.animationExists = false;}, 112 | this.absorptionDuration 113 | ); 114 | // Make text groups disappear 115 | window.setTimeout( 116 | this.removeTexts.bind(this), 117 | absorptionDuration + absorptionTextDuration 118 | ); 119 | } 120 | 121 | displayMeasurementTexts(stepNo) { 122 | _.forEach(this.measurementHistory[stepNo], (measurement) => { 123 | this.measurementTextGroup.datum(measurement) 124 | .append('text') 125 | .attr('class', 'measurement-text unselectable') 126 | .attr('x', (d) => tileSize * d.i + tileSize / 2) 127 | .attr('y', (d) => tileSize * d.j + tileSize / 2) 128 | .attr('dy', '0.5em') 129 | .style('font-size', '20px') 130 | .text((d) => d.measured ? 'click!' : 'not here...') 131 | .transition().duration(2 * this.animationStepDuration) 132 | .style('font-size', '60px') 133 | .style('opacity', 0) 134 | .remove(); 135 | 136 | this.measurementTextGroup.datum(measurement) 137 | .each((d) => { 138 | if (d.measured && d.tile != null) { 139 | d.tile.absorbSound(); 140 | d.tile.absorbAnimation(); 141 | } 142 | }); 143 | }); 144 | 145 | } 146 | 147 | displayAbsorptionTexts() { 148 | // TODO(pmigdal): instead of texts - a heatmap of colorful tiles? 149 | this.absorptionTextGroup 150 | .selectAll('.absorption-text') 151 | .data(this.absorptionProbabilities) 152 | .enter() 153 | .append('text') 154 | .attr('class', 'absorption-text unselectable') 155 | .attr('x', (d) => tileSize * d.i + tileSize) 156 | .attr('y', (d) => tileSize * d.j + tileSize) 157 | .attr('dx', '-0.1em') 158 | .attr('dy', '-0.1em') 159 | .text((d) => (100 * d.probability).toFixed(0) + '%') 160 | .transition().duration(absorptionTextDuration) 161 | .style('opacity', 0) 162 | .remove(); 163 | 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /js/particle/particle_animation.spec.js: -------------------------------------------------------------------------------- 1 | import {MockD3} from '../test_utils/mock_d3'; 2 | import {ParticleAnimation} from './particle_animation'; 3 | 4 | describe('Particle animation', () => { 5 | let dummyAnimation; 6 | let finishCallback; 7 | let mockBoard; 8 | 9 | beforeEach(() => { 10 | mockBoard = { 11 | svg: new MockD3(), 12 | }; 13 | finishCallback = jasmine.createSpy(null); 14 | // Allow d3 mock to be chainable 15 | spyOn(mockBoard.svg, 'append').and.callThrough(); 16 | spyOn(mockBoard.svg, 'attr').and.callThrough(); 17 | 18 | dummyAnimation = new ParticleAnimation( 19 | mockBoard, 20 | [], // history 21 | [], // measurementHistory 22 | [], // absorptionProbabilities 23 | finishCallback, 24 | 'defaultMode' 25 | ); 26 | spyOn(dummyAnimation, 'nextFrame'); 27 | }); 28 | 29 | it('should initialize by playing and uninitialize by stopping', () => { 30 | expect(dummyAnimation.initialized).toBe(false); 31 | dummyAnimation.play(); 32 | expect(dummyAnimation.initialized).toBe(true); 33 | dummyAnimation.pause(); 34 | expect(dummyAnimation.initialized).toBe(true); 35 | dummyAnimation.stop(); 36 | expect(dummyAnimation.initialized).toBe(false); 37 | }); 38 | 39 | it('should call nextFrame by plaing', () => { 40 | dummyAnimation.play(); 41 | expect(dummyAnimation.nextFrame).toHaveBeenCalled(); 42 | }); 43 | 44 | it('should stop playing by pausing', () => { 45 | expect(dummyAnimation.playing).toBe(false); 46 | dummyAnimation.play(); 47 | expect(dummyAnimation.playing).toBe(true); 48 | dummyAnimation.pause(); 49 | expect(dummyAnimation.playing).toBe(false); 50 | }); 51 | 52 | it('should create and remove measurement and absorption texts', () => { 53 | dummyAnimation.play(); 54 | expect(dummyAnimation.measurementTextGroup).toBeTruthy(); 55 | // Check which element was removed 56 | spyOn(mockBoard.svg, 'remove'); 57 | spyOn(dummyAnimation.measurementTextGroup, 'remove'); 58 | spyOn(dummyAnimation.absorptionTextGroup, 'remove'); 59 | dummyAnimation.stop(); 60 | expect(mockBoard.svg.remove).not.toHaveBeenCalled(); 61 | expect(dummyAnimation.measurementTextGroup.remove).toHaveBeenCalled(); 62 | expect(dummyAnimation.absorptionTextGroup.remove).toHaveBeenCalled(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /js/particle/svg_particle_animation.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import _ from 'lodash'; 3 | import d3 from 'd3'; 4 | 5 | import {TAU, perpendicularI, perpendicularJ} from '../const'; 6 | import {oscillations, polarizationScaleH, polarizationScaleV} from '../config'; 7 | import {ParticleAnimation} from './particle_animation'; 8 | 9 | export class SVGParticleAnimation extends ParticleAnimation { 10 | constructor(board, history, measurementHistory, absorptionProbabilities, interruptCallback, finishCallback, drawMode, displayMessage) { 11 | super(board, history, measurementHistory, absorptionProbabilities, interruptCallback, finishCallback, drawMode, displayMessage); 12 | this.particleGroup = null; 13 | this.currentTimeout = 0; 14 | } 15 | 16 | pause() { 17 | super.pause(); 18 | window.clearTimeout(this.currentTimeout); 19 | } 20 | 21 | stop() { 22 | super.stop(); 23 | this.exitParticles(); 24 | } 25 | 26 | initialize() { 27 | super.initialize(); 28 | this.particleGroup = this.board.svg 29 | .append('g') 30 | .attr('class', 'particles'); 31 | } 32 | 33 | finish() { 34 | super.finish(); 35 | this.exitParticles(); 36 | } 37 | 38 | /** 39 | * Make next frame of animation, possibly setting the timeout for the 40 | * next frame of animation. 41 | */ 42 | nextFrame() { 43 | this.updateParticles(); 44 | this.displayMeasurementTexts(this.stepNo); 45 | this.stepNo++; 46 | 47 | if (this.stepNo < this.history.length - 1) { 48 | // Set timeout only if playing 49 | if (this.playing) { 50 | this.currentTimeout = window.setTimeout( 51 | this.nextFrame.bind(this), 52 | this.animationStepDuration 53 | ); 54 | } 55 | } else { 56 | this.finish(); 57 | } 58 | } 59 | 60 | updateParticles() { 61 | const particles = this.particleGroup 62 | .selectAll('.particle') 63 | .data(this.history[this.stepNo]); 64 | 65 | particles 66 | .exit() 67 | .remove(); 68 | 69 | particles 70 | .enter() 71 | .append('use') 72 | .attr({ 73 | 'xlink:href': '#particle', 74 | 'class': 'particle', 75 | }); 76 | 77 | particles 78 | .attr('transform', (d) => `translate(${d.startX},${d.startY})`) 79 | .style('opacity', (d) => Math.sqrt(d.prob)); 80 | 81 | particles 82 | .interrupt() 83 | .transition() 84 | .ease([0, 1]) 85 | .duration(this.animationStepDuration) 86 | .attrTween('transform', (d) => (t) => { 87 | const h = polarizationScaleH * (d.hRe * Math.cos(oscillations * TAU * t) + d.hIm * Math.sin(oscillations * TAU * t)) / Math.sqrt(d.prob); 88 | const x = (1 - t) * d.startX + t * d.endX + perpendicularI[d.dir] * h; 89 | const y = (1 - t) * d.startY + t * d.endY + perpendicularJ[d.dir] * h; 90 | const s = 1 + polarizationScaleV * (d.vRe * Math.cos(oscillations * TAU * t) + d.vIm * Math.sin(oscillations * TAU * t)) / Math.sqrt(d.prob); 91 | return `translate(${x}, ${y}) scale(${s})`; 92 | }); 93 | }; 94 | 95 | exitParticles() { 96 | this.particleGroup.selectAll('.particle') 97 | .transition().duration(this.animationStepDuration) 98 | .style('opacity', 0) 99 | .delay(this.animationStepDuration) 100 | .remove(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /js/popup_manager.js: -------------------------------------------------------------------------------- 1 | export class PopupManager { 2 | constructor(popupElem, nextLevelCallback) { 3 | this.popupElem = popupElem; 4 | this.nextLevel = nextLevelCallback; 5 | this.bindEvents(); 6 | } 7 | 8 | toggle(shown, buttons) { 9 | this.popupElem.classed('popup--shown', shown); 10 | } 11 | 12 | popup(content, buttons) { 13 | this.popupElem.select('.popup-content') 14 | .html(content); 15 | // Toggle button visibility 16 | this.popupElem.select('.popup-buttons .popup-action--close') 17 | .classed('hidden', !buttons.close); 18 | this.popupElem.select('.popup-buttons .popup-action--next-level') 19 | .classed('hidden', !buttons.nextLevel); 20 | this.toggle(true); 21 | } 22 | 23 | bindEvents() { 24 | const popupManager = this; 25 | this.popupElem.selectAll('.popup-action--close') 26 | .on('click', () => { 27 | popupManager.toggle(false); 28 | }); 29 | this.popupElem.selectAll('.popup-action--next-level') 30 | .on('click', () => { 31 | popupManager.toggle(false); 32 | this.nextLevel(); 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /js/print.js: -------------------------------------------------------------------------------- 1 | // displaying and printing states, operators etc 2 | // as of now mostly for debugging purpose 3 | 4 | export const componentToStr = (component) => { 5 | let amplitudeStr = ''; 6 | 7 | if (component.re !== 0 && component.im !== 0) { 8 | if (component.im > 0) { 9 | amplitudeStr = `(${component.re.toFixed(3)} + ${component.im.toFixed(3)}i)`; 10 | } else { 11 | amplitudeStr = `(${component.re.toFixed(3)} - ${Math.abs(component.im).toFixed(3)}i)`; 12 | } 13 | } else if (component.re === 0) { 14 | amplitudeStr = `(${component.im.toFixed(3)}i)`; 15 | } else if (component.im === 0) { 16 | amplitudeStr = `(${component.re.toFixed(3)})`; 17 | } 18 | 19 | return `${amplitudeStr}*|${component.i},${component.j},${component.to})`; 20 | }; 21 | 22 | export const stateToStr = (state) => state.map(componentToStr).join(' + '); 23 | 24 | // NOTE(migdal) switched off katex for now; I will reload once it is actually being used 25 | //// NOTE right now it is only for the direction-polarization basis 26 | // export const tensorToLaTeX = (tensor) => { 27 | // const basis = ['>-', '>|', '^-', '^|', '<-', '<|', 'v-', 'v|']; 28 | // const arrayContent = basis 29 | // .map((outputBase) => basis 30 | // .map((inputBase) => { 31 | // let matrixElement = tensor.get(inputBase).get(outputBase); 32 | // if (matrixElement === undefined || (matrixElement.re === 0 && matrixElement.im === 0)) { 33 | // return '0'; 34 | // } else { 35 | // if (matrixElement.re !== 0 && matrixElement.im !== 0) { 36 | // if (matrixElement.im > 0) { 37 | // return `${matrixElement.re.toFixed(3)} + ${matrixElement.im.toFixed(3)}i`; 38 | // } else { 39 | // return `${matrixElement.re.toFixed(3)} - ${Math.abs(matrixElement.im.toFixed(3))}i`; 40 | // } 41 | // } else if (matrixElement.re === 0) { 42 | // return `${matrixElement.im.toFixed(3)}i`; 43 | // } else if (matrixElement.im === 0) { 44 | // return `${matrixElement.re.toFixed(3)}`; 45 | // } 46 | // } 47 | // } 48 | // ).join(' & ') 49 | // ) 50 | // .join('\\\\'); 51 | // return katex.renderToString(`\\begin{bmatrix}${arrayContent}\\end{bmatrix}`); 52 | // }; 53 | 54 | export const absorbedToStr = (absorbed) => 55 | absorbed 56 | .map((a) => 57 | `${a.measured ? '!!!' : '...'} ${(100 * a.probability).toFixed(0)}% (${a.i},${a.j}) ${a.tile != null ? a.tile.tileName : 'out'}` 58 | ) 59 | .join('\n'); 60 | -------------------------------------------------------------------------------- /js/progress_pearls.js: -------------------------------------------------------------------------------- 1 | import {tileSize, pearlsPerRow} from './config'; 2 | 3 | const pearlRadius = 0.2 * tileSize; 4 | const pearlDistance = 0.5 * tileSize; 5 | 6 | export class ProgressPearls { 7 | 8 | constructor(selector, levels, gameBoard) { 9 | this.g = selector.append('g') 10 | .attr('class', 'progress-pearls'); 11 | this.levels = levels; 12 | this.gameBoard = gameBoard; 13 | } 14 | 15 | draw() { 16 | this.pearls = this.g.selectAll('.pearl') 17 | .data(this.levels); 18 | 19 | const pearlsEntered = this.pearls.enter() 20 | .append('g') 21 | .attr('class', 'pearl') 22 | .attr('transform', (d, i) => `translate(${pearlDistance * (i % pearlsPerRow + 0.5)}, ${pearlDistance * (Math.floor(i / pearlsPerRow) - 0.75)})`) 23 | .on('click', (d) => { 24 | this.gameBoard.loadLevel(d.id); 25 | }); 26 | 27 | pearlsEntered.append('circle') 28 | .attr('r', pearlRadius); 29 | 30 | pearlsEntered.append('text') 31 | .text((d) => d.i); 32 | 33 | this.update(); 34 | } 35 | 36 | update() { 37 | 38 | // TODO(migdal) accesible levels 39 | 40 | const isWon = (d) => this.gameBoard.storage.getLevelIsWon(d.id); 41 | 42 | this.pearls 43 | .classed('pearl--passed', isWon) 44 | .classed('pearl--current', (d) => d.id === this.gameBoard.storage.getCurrentLevelId()) 45 | .on('mouseover', (d) => { 46 | this.gameBoard.titleManager.displayMessage( 47 | `GO TO: ${d.i}. ${d.name} ${isWon(d) ? '[won]' : ''}`, 48 | '' 49 | ) 50 | }); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /js/simulation.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import _ from 'lodash'; 3 | 4 | import {EPSILON, velocityI, velocityJ} from './const'; 5 | import {maxIterations} from './config'; 6 | import * as print from './print'; 7 | 8 | const zAbs = (z) => 9 | z.re * z.re + z.im * z.im; 10 | 11 | const intensityPerPosition = (state) => 12 | _(state) 13 | .groupBy((entry) => `${entry.i} ${entry.j}`) 14 | .mapValues((groupedEntry) => 15 | _.sumBy(groupedEntry, zAbs) 16 | ) 17 | .value(); 18 | 19 | export class Simulation { 20 | 21 | constructor(tileMatrix, logging) { 22 | this.tileMatrix = tileMatrix; 23 | this.levelHeight = Math.max(...this.tileMatrix.map((row) => row.length || 0)); 24 | this.levelWidth = this.tileMatrix.length; 25 | this.history = []; 26 | this.measurementHistory = []; 27 | this.logging = (logging === 'logging'); 28 | } 29 | 30 | /** 31 | * Clear history and make it one-element list 32 | * containing initial particles state. 33 | */ 34 | initialize() { 35 | 36 | const initialState = 37 | _.reduce(_.range(this.levelWidth), (accI, i) => { 38 | return _.reduce(_.range(this.levelHeight), (accJ, j) => { 39 | // Recognize generating tiles by having 'generation' method 40 | if (!this.tileMatrix[i][j].type.generation) { 41 | return accJ; 42 | } 43 | const emissions = 44 | this.tileMatrix[i][j].type.generation( 45 | this.tileMatrix[i][j].rotation 46 | ); 47 | _.forEach(emissions, (emission) => { 48 | accJ.push({i: i, 49 | j: j, 50 | to: emission.to, 51 | re: emission.re, 52 | im: emission.im, 53 | }); 54 | }); 55 | return accJ; 56 | }, accI); 57 | }, []); 58 | 59 | if (this.logging) { 60 | window.console.log('Simulation started:'); 61 | window.console.log(print.stateToStr(initialState)); 62 | } 63 | 64 | this.history.push(initialState); 65 | this.measurementHistory.push([]); 66 | this.noClickYet = true; 67 | } 68 | 69 | /** 70 | * Make one propagation step and save it in history. 71 | * Additionally, return it. 72 | */ 73 | propagate(quantum, onlyDetectors = -1) { 74 | 75 | const lastState = _.last(this.history); 76 | const displacedState = this.displace(lastState); 77 | let newState = this.interact(displacedState); 78 | const absorbed = this.absorb(displacedState, newState, onlyDetectors); 79 | 80 | if (quantum && onlyDetectors < 0) { 81 | newState = this.normalize(newState); 82 | } 83 | 84 | this.history.push(newState); 85 | this.measurementHistory.push(absorbed); 86 | 87 | if (this.logging) { 88 | window.console.log(print.stateToStr(displacedState)); 89 | if (absorbed.length > 0) { 90 | window.console.log(print.absorbedToStr(absorbed)); 91 | } 92 | } 93 | 94 | if (_.some(absorbed, 'measured') && quantum) { 95 | return []; 96 | } else { 97 | return newState; 98 | } 99 | 100 | } 101 | 102 | /** 103 | * Creates a new state basing on input state, with particles 104 | * moved according to their directions. 105 | */ 106 | // WARNING: creating may be slower than just modifying i and j 107 | displace(state) { 108 | return _.map(state, (entry) => { 109 | // 'to' value = direction + polarization 110 | const dir = entry.to[0]; 111 | const newI = entry.i + velocityI[dir]; 112 | const newJ = entry.j + velocityJ[dir]; 113 | return {i: newI, 114 | j: newJ, 115 | to: entry.to, 116 | re: entry.re, 117 | im: entry.im, 118 | }; 119 | }); 120 | } 121 | 122 | absorb(stateOld, stateNew, onlyDetectors = -1) { 123 | 124 | const intensityOld = intensityPerPosition(stateOld); 125 | const intensityNew = intensityPerPosition(stateNew); 126 | 127 | const bins = _(intensityOld) 128 | .mapValues((prob, location) => 129 | prob - (intensityNew[location] || 0) 130 | ) 131 | .pickBy((prob) => prob > EPSILON) 132 | .map((prob, location) => { 133 | return { 134 | probability: prob, 135 | measured: false, 136 | i: parseInt(location.split(' ')[0]), 137 | j: parseInt(location.split(' ')[1]), 138 | }; 139 | }) 140 | .value(); 141 | 142 | bins.forEach((each) => { 143 | each.tile = this.tileMatrix[each.i] && this.tileMatrix[each.i][each.j]; 144 | }); 145 | 146 | 147 | const rand = Math.random(); 148 | 149 | let probSum = 0; 150 | if (this.noClickYet) { 151 | if (onlyDetectors > 0) { 152 | // the cheated variant 153 | for (let k = 0; k < bins.length; k++) { 154 | if (bins[k].tile.isDetector) { 155 | probSum += bins[k].probability * onlyDetectors; 156 | if (probSum > rand) { 157 | bins[k].measured = true; 158 | this.noClickYet = false; 159 | break; 160 | } 161 | } 162 | } 163 | } else { 164 | // usual variarant 165 | for (let k = 0; k < bins.length; k++) { 166 | probSum += bins[k].probability; 167 | if (probSum > rand) { 168 | bins[k].measured = true; 169 | this.noClickYet = false; 170 | break; 171 | } 172 | } 173 | } 174 | } 175 | 176 | return bins; 177 | 178 | } 179 | 180 | /** 181 | * Creates a new state basing on input state, applying probability 182 | * function changes from tiles' interactions. 183 | */ 184 | interact(state) { 185 | // Collect all transitions into bins. Each bin will be labeled 186 | // with position (i, j) and momentum direction. 187 | const bins = _.reduce(state, (acc, entry) => { 188 | // Check if particle is out of bound 189 | if ( 190 | entry.i < 0 || entry.i >= this.levelWidth 191 | || entry.j < 0 || entry.j >= this.levelHeight 192 | ) { 193 | return acc; 194 | } 195 | const tile = this.tileMatrix[entry.i][entry.j]; 196 | 197 | const transition = tile.transitionAmplitudes.map.get(entry.to); 198 | for (let [to, change] of transition) { 199 | const binKey = [entry.i, entry.j, to].join('_'); 200 | // (a + bi)(c + di) = (ac - bd) + i(ad + bc) 201 | const re = entry.re * change.re - entry.im * change.im; 202 | const im = entry.re * change.im + entry.im * change.re; 203 | // Add to bin 204 | if (_.has(acc, binKey)) { 205 | acc[binKey].re += re; 206 | acc[binKey].im += im; 207 | } else { 208 | acc[binKey] = {i: entry.i, 209 | j: entry.j, 210 | to: to, 211 | re: re, 212 | im: im, 213 | }; 214 | } 215 | } 216 | return acc; 217 | }, {}); 218 | // Remove keys; filter out zeroes 219 | return _.values(bins).filter((entry) => 220 | entry.re * entry.re + entry.im * entry.im > EPSILON 221 | ); 222 | } 223 | 224 | normalize(state) { 225 | 226 | let norm = _.chain(state) 227 | .map((entry) => entry.re * entry.re + entry.im * entry.im) 228 | .sum(); 229 | 230 | norm = Math.sqrt(norm); 231 | 232 | return state.map((entry) => 233 | _.assign(entry, { 234 | re: entry.re / norm, 235 | im: entry.im / norm, 236 | }) 237 | ); 238 | 239 | } 240 | 241 | /** 242 | * Propagate until: 243 | * - all probabilities go to 0 244 | * - iteration limit is reached 245 | */ 246 | propagateToEnd(quantum = true) { 247 | let stepNo, lastStep; 248 | for (stepNo = 0; stepNo < maxIterations; ++stepNo) { 249 | lastStep = this.propagate(quantum); 250 | if (!lastStep.length) { 251 | break; 252 | } 253 | } 254 | } 255 | 256 | // propagation making sure that it will click at one of the detectors 257 | propagateToEndCheated(absAtDetByTime) { 258 | const totalDetection = _.sum(absAtDetByTime); 259 | let detectionSoFar = 0; 260 | let stepNo, lastStep; 261 | for (stepNo = 0; stepNo < absAtDetByTime.length; ++stepNo) { 262 | lastStep = this.propagate(true, 1 / (totalDetection - detectionSoFar )); 263 | detectionSoFar += absAtDetByTime[stepNo+1]; 264 | if (!lastStep.length) { 265 | break; 266 | } 267 | } 268 | 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /js/sound_service.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as soundjs from 'soundjs'; 3 | 4 | const SOUND_DEFS = { 5 | blip: { 6 | file: 'blip.mp3', 7 | throttleMs: 100, 8 | }, 9 | error: { 10 | file: 'error.mp3', 11 | throttleMs: 250, 12 | }, 13 | detector: { 14 | file: 'detector.mp3', 15 | throttleMs: 100, 16 | }, 17 | mine: { 18 | file: 'mine.mp3', 19 | throttleMs: 1000, 20 | }, 21 | rock: { 22 | file: 'rock.mp3', 23 | throttleMs: 1000, 24 | }, 25 | absorber: { 26 | file: 'absorber.mp3', 27 | throttleMs: 1000, 28 | }, 29 | }; 30 | 31 | 32 | export class SoundService { 33 | static initialize() { 34 | if (SoundService.initialized) { 35 | return; 36 | } 37 | // Register sounds 38 | _.forIn(SOUND_DEFS, (def, name) => { 39 | soundjs.Sound.registerSound(`/sounds/${def.file}`, name); 40 | }); 41 | // Create throttled versions 42 | SoundService.throttled = _.mapValues(SOUND_DEFS, (def, name) => { 43 | return _.throttle( 44 | () => { 45 | soundjs.Sound.play(name); 46 | }, 47 | def.throttleMs, 48 | { 49 | leading: true, 50 | trailing: false, 51 | }); 52 | }); 53 | SoundService.initialized = true; 54 | } 55 | 56 | static play(name) { 57 | soundjs.Sound.play(name); 58 | } 59 | 60 | static playThrottled(name) { 61 | SoundService.throttled[name](); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /js/stock.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import d3 from 'd3'; 3 | 4 | import * as tile from './tile'; 5 | import {tileSize, tileBorder, stockHeight} from './config'; 6 | import {bindDrag} from './drag_and_drop'; 7 | 8 | export class Stock { 9 | constructor(svg, board) { 10 | this.svg = svg; 11 | this.board = board; 12 | } 13 | 14 | elementCount(level) { 15 | this.stock = level.initialStock; 16 | 17 | // initialize 0-count stock for non-frozen tiles on board 18 | level.tileRecipes.forEach((tileRecipe) => { 19 | if (!tileRecipe.frozen && !_.has(this.stock, tileRecipe.name)) { 20 | this.stock[tileRecipe.name] = 0; 21 | } 22 | }); 23 | 24 | this.usedTileNames = _.keys(this.stock); // add some ordering to the stock? 25 | this.level = level; 26 | } 27 | 28 | drawStock() { 29 | 30 | // Reset element 31 | this.svg.select('.stock').remove(); 32 | this.stockGroup = this.svg 33 | .append('g') 34 | .attr('class', 'stock'); 35 | 36 | // Create background 37 | const maxRows = stockHeight; 38 | const iShift = this.level.width + 1; 39 | 40 | const dataForStockDrawing = _.map(this.usedTileNames, (name, i) => ({ 41 | name: name, 42 | i: Math.floor(i / maxRows) + iShift, 43 | j: i % maxRows, 44 | })); 45 | 46 | this.stockSlots = this.stockGroup 47 | .selectAll('.stock-slot') 48 | .data(dataForStockDrawing); 49 | 50 | const stockSlotsEntered = this.stockSlots.enter() 51 | .append('g') 52 | .attr('class', 'stock-slot') 53 | .classed('stock-empty', (d) => this.stock[d.name] <= 0); 54 | 55 | stockSlotsEntered.append('rect') 56 | .attr('class', 'background-tile') 57 | .attr('width', tileSize - 2 * tileBorder) 58 | .attr('height', tileSize - 2 * tileBorder) 59 | .attr('transform', (d) => `translate(${d.i * tileSize + tileBorder},${d.j * tileSize + tileBorder})`); 60 | 61 | stockSlotsEntered.append('text') 62 | .attr('class', 'stock-count unselectable') 63 | .attr('transform', (d) => `translate(${(d.i + 0.9) * tileSize},${(d.j + 0.9) * tileSize})`) 64 | .text((d) => `x ${this.stock[d.name]}`); 65 | 66 | this.regenerateTile(stockSlotsEntered); 67 | } 68 | 69 | regenerateTile(stockSlotG) { 70 | 71 | const newTile = stockSlotG.append('g') 72 | .datum((d) => new tile.Tile(tile[d.name], 0, false, d.i, d.j)) 73 | .attr('class', 'tile') 74 | .attr('transform', (d) => `translate(${d.x + tileSize / 2},${d.y + tileSize / 2})`) 75 | .each(function (tileObj) { 76 | tileObj.g = d3.select(this); 77 | tileObj.node = this; 78 | tileObj.fromStock = true; 79 | tileObj.draw(); 80 | }); 81 | 82 | newTile.append('rect') 83 | .attr('class', 'hitbox') 84 | .attr('x', -tileSize / 2) 85 | .attr('y', -tileSize / 2) 86 | .attr('width', tileSize) 87 | .attr('height', tileSize) 88 | .on('mouseover', this.board.callbacks.tileMouseover); 89 | 90 | bindDrag(newTile, this.board, this); 91 | 92 | } 93 | 94 | updateCount(tileName, change) { 95 | 96 | this.stock[tileName] += change; 97 | 98 | this.stockSlots 99 | .classed('stock-empty', (d) => this.stock[d.name] <= 0); 100 | 101 | this.stockSlots.select('text') 102 | .text((d) => `x ${this.stock[d.name]}`); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /js/storage.js: -------------------------------------------------------------------------------- 1 | export class Storage { 2 | constructor() { 3 | this.ls = window.localStorage; 4 | } 5 | 6 | setLevelProgress(levelId, boardExport) { 7 | this.ls.setItem( 8 | `LevelProgress ${levelId}`, 9 | JSON.stringify(boardExport) 10 | ); 11 | } 12 | 13 | hasLevelProgress(levelId) { 14 | return this.ls.hasOwnProperty(`LevelProgress ${levelId}`); 15 | } 16 | 17 | getLevelProgress(levelId) { 18 | const content = this.ls.getItem(`LevelProgress ${levelId}`); 19 | if (content == null) { 20 | throw new Error(`No data for levelId: ${levelId}`); 21 | } 22 | return JSON.parse(this.ls.getItem(`LevelProgress ${levelId}`)); 23 | } 24 | 25 | setLevelIsWon(levelId, value = true) { 26 | this.ls.setItem(`LevelIsWon ${levelId}`, String(value)); 27 | } 28 | 29 | getLevelIsWon(levelId) { 30 | return this.ls.getItem(`LevelIsWon ${levelId}`) === 'true'; 31 | } 32 | 33 | setCurrentLevelId(levelId) { 34 | this.ls.setItem('CurrentLevelId', levelId); 35 | } 36 | 37 | getCurrentLevelId() { 38 | return this.ls.getItem('CurrentLevelId'); 39 | } 40 | 41 | // TODO(migdal) accesible levels 42 | 43 | } 44 | -------------------------------------------------------------------------------- /js/tensor/direction.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {Tensor} from './tensor'; 4 | 5 | // Moving directions. We allow only four of them: 6 | export const directions = ['>', '^', '<', 'v']; 7 | 8 | export function directionToAngle(direction){ 9 | return { 10 | '>': 0, 11 | '^': 90, 12 | '<': 180, 13 | 'v': 270, 14 | }[direction]; 15 | } 16 | export function angleToDirection(angle) { 17 | return { 18 | '0': '>', 19 | '90': '^', 20 | '180': '<', 21 | '270': 'v', 22 | }['' + angle]; 23 | } 24 | 25 | export const identity = Tensor.fill(directions, {re: 1, im: 0}); 26 | export const zero = Tensor.fill(directions, {re: 0, im: 0}); 27 | 28 | // Reflection direction: reflecting from point 29 | export function pointReflectionDirection(direction) { 30 | const incidentAngle = directionToAngle(direction); 31 | const reflectedAngle = (incidentAngle + 180) % 360; 32 | return angleToDirection(reflectedAngle); 33 | } 34 | 35 | // Reflection direction basing on plane's rotation (- / | \) 36 | export function planeReflectionDirection(direction, rotation) { 37 | const mirrorPlaneAngle = rotation * 45; 38 | const incidentAngle = directionToAngle(direction); 39 | const reflectedAngle = (2 * mirrorPlaneAngle - incidentAngle + 360) % 360; 40 | return angleToDirection(reflectedAngle); 41 | } 42 | 43 | export const cube = Tensor.fromObject( 44 | _.reduce(directions, (acc, dirFrom) => { 45 | const dirTo = pointReflectionDirection(dirFrom); 46 | acc[dirFrom] = {}; 47 | acc[dirFrom][dirTo] = {re: 1, im: 0}; 48 | return acc; 49 | }, {}) 50 | ); 51 | 52 | export const mirror = _.range(4).map((rotation) => { 53 | return Tensor.fromObject( 54 | _.reduce(directions, (acc, dirFrom) => { 55 | const dirTo = planeReflectionDirection(dirFrom, rotation); 56 | acc[dirFrom] = {}; 57 | if (dirFrom !== dirTo) { 58 | acc[dirFrom][dirTo] = {re: 1, im: 0}; 59 | } 60 | return acc; 61 | }, {}) 62 | ); 63 | }); 64 | 65 | export const mirrorCoated = _.range(8).map((rotation) => { 66 | return Tensor.fromObject( 67 | _.reduce(directions, (acc, dirFrom, iFrom) => { 68 | const dirTo = planeReflectionDirection(dirFrom, rotation); 69 | const sign = (-rotation/2 + iFrom + 8) % 4 < 1.75 ? -1 : 1; 70 | acc[dirFrom] = {}; 71 | if (dirFrom !== dirTo) { 72 | acc[dirFrom][dirTo] = {re: sign, im: 0}; 73 | } 74 | return acc; 75 | }, {}) 76 | ); 77 | }); 78 | 79 | export const diode = _.range(4).map((rotation) => { 80 | return Tensor.fromObject( 81 | _.reduce(directions, (acc, dirFrom) => { 82 | acc[dirFrom] = {}; 83 | if (dirFrom === directions[rotation]) { 84 | acc[dirFrom][dirFrom] = {re: 1, im: 0}; 85 | } 86 | return acc; 87 | }, {}) 88 | ); 89 | }); 90 | 91 | export const absorbOneDirReflectOther = _.range(4).map((rotation) => { 92 | return Tensor.fromObject( 93 | _.reduce(directions, (acc, dirFrom, iFrom) => { 94 | const dirTo = pointReflectionDirection(dirFrom); 95 | acc[dirFrom] = {}; 96 | if (rotation !== iFrom) { 97 | acc[dirFrom][dirTo] = {re: 1, im: 0}; 98 | } 99 | return acc; 100 | }, {}) 101 | ); 102 | }); 103 | -------------------------------------------------------------------------------- /js/tensor/full.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {Tensor} from './tensor'; 4 | import * as direction from './direction'; 5 | import * as polarization from './polarization'; 6 | import {TAU} from '../const'; 7 | 8 | /** 9 | * Module contains (mostly) transition probabilities. 10 | * Some of them are independent of tile orientation - in this case 11 | * the probability information is represented as tensor. 12 | * If there's a dependency between orientation and probability, 13 | * there appears a list of tensors, one for each orientation. 14 | */ 15 | 16 | export const identity = Tensor.product( 17 | direction.identity, 18 | polarization.identity 19 | ); 20 | 21 | export const zero = Tensor.product( 22 | direction.zero, 23 | polarization.zero 24 | ); 25 | 26 | const pipeH = Tensor.product( 27 | Tensor.sum( 28 | direction.diode[0], 29 | direction.diode[2] 30 | ), 31 | polarization.identity 32 | ); 33 | 34 | const pipeV = Tensor.product( 35 | Tensor.sum( 36 | direction.diode[1], 37 | direction.diode[3] 38 | ), 39 | polarization.identity 40 | ); 41 | 42 | const pipes = [pipeH, pipeV]; 43 | 44 | // TODO Following thing is not a Tensor. 45 | // TODO Make it easy to distinguish types of things. 46 | export const source = _.range(4).map((rotation) => { 47 | return [{ 48 | to: `${direction.directions[rotation]}|`, 49 | re: 1.0, 50 | im: 0.0, 51 | }]; 52 | }); 53 | 54 | export const detector = _.range(4).map((rotation) => 55 | Tensor.product( 56 | direction.absorbOneDirReflectOther[rotation], 57 | polarization.reflectPhaseFromDenser 58 | ) 59 | ); 60 | 61 | export const cornerCube = Tensor.product( 62 | direction.cube, 63 | polarization.identity 64 | ); 65 | 66 | export const thinMirror = _.range(4).map((rotation) => 67 | Tensor.product( 68 | direction.mirror[rotation], 69 | polarization.reflectPhaseFromDenser 70 | ) 71 | ); 72 | 73 | // FIX(migdal) this one is not even unitary 74 | export const thinMirrorCoated = _.range(8).map((rotation) => 75 | Tensor.product( 76 | direction.mirrorCoated[rotation], 77 | polarization.reflectPhaseFromDenser 78 | ) 79 | ); 80 | 81 | export const thinSplitter = _.range(4).map((rotation) => 82 | Tensor.sum( 83 | Tensor.byConstant( 84 | rotation % 2 === 1 ? identity : pipes[(rotation / 2 + 1) % 2], 85 | {re: Math.SQRT1_2, im: 0} 86 | ), 87 | Tensor.byConstant( 88 | thinMirror[rotation], 89 | {re: 0, im: -Math.SQRT1_2} 90 | ) 91 | ) 92 | ); 93 | 94 | export const thinSplitterCoated = _.range(8).map((rotation) => 95 | Tensor.sum( 96 | Tensor.byConstant( 97 | rotation % 2 === 1 ? identity : pipes[(rotation / 2 + 1) % 2], 98 | {re: Math.SQRT1_2, im: 0} 99 | ), 100 | Tensor.byConstant( 101 | thinMirrorCoated[rotation], 102 | {re: Math.SQRT1_2, im: 0} 103 | ) 104 | ) 105 | ); 106 | 107 | export const polarizingSplitter = _.range(2).map((rotation) => { 108 | // Convert polarizing splitter rotation (/ \) into mirror rotation (- / | \) 109 | const mirrorRotation = 2 * rotation + 1; 110 | return Tensor.fromObject(_.reduce(direction.directions, (acc, dir) => { 111 | const reflectedDirection = direction.planeReflectionDirection(dir, mirrorRotation); 112 | // Polarization - passes through 113 | acc[`${dir}-`] = {}; 114 | acc[`${dir}-`][`${dir}-`] = {re: 1, im: 0}; 115 | // Polarization | gets reflected 116 | acc[`${dir}|`] = {}; 117 | acc[`${dir}|`][`${reflectedDirection}|`] = {re: 1, im: 0}; 118 | return acc; 119 | }, {})); 120 | }); 121 | 122 | // TODO check sign (?) 123 | // Quarter wave-plate 124 | export const glass = Tensor.product( 125 | direction.identity, 126 | polarization.globalPhase(TAU / 4) 127 | ); 128 | 129 | // Quarter wave-plate phase, but with opposite sign 130 | export const vacuumJar = Tensor.product( 131 | direction.identity, 132 | polarization.globalPhase(-TAU / 4) 133 | ); 134 | 135 | 136 | export const absorber = Tensor.product( 137 | direction.identity, 138 | polarization.globalAbsorption(0.5) 139 | ); 140 | 141 | // TODO check sign 142 | export const sugarSolution = Tensor.product( 143 | direction.identity, 144 | polarization.rotation(TAU / 8) 145 | ); 146 | 147 | export const doubleSugarSolution = Tensor.product( 148 | direction.identity, 149 | polarization.rotation(TAU / 4) 150 | ); 151 | 152 | // TODO make the formula easier or at least understand it 153 | const covariantAngle = (elementRotation, lightDirection) => 154 | (1 - (lightDirection & 2)) * (1 - 2 * (lightDirection & 1)) * (-elementRotation - 2 * lightDirection) * TAU / 8; 155 | 156 | export const polarizer = _.range(4).map((rotation) => 157 | Tensor.sumList( 158 | direction.diode.map((directionGo, i) => 159 | Tensor.product( 160 | directionGo, 161 | polarization.projection(covariantAngle(rotation, i)) 162 | ) 163 | ) 164 | ) 165 | ); 166 | 167 | export const polarizerNS = _.range(4).map((rotation) => 168 | Tensor.sumList( 169 | direction.diode.map((directionGo, i) => { 170 | if (i === 1 || i === 3) { 171 | return Tensor.product( 172 | directionGo, 173 | polarization.projection(covariantAngle(rotation, i)) 174 | ); 175 | } else { 176 | return Tensor.product( 177 | directionGo, 178 | polarization.zero 179 | ); 180 | } 181 | }) 182 | ) 183 | ); 184 | 185 | export const polarizerWE = _.range(4).map((rotation) => 186 | Tensor.sumList( 187 | direction.diode.map((directionGo, i) => { 188 | if (i === 0 || i === 2) { 189 | return Tensor.product( 190 | directionGo, 191 | polarization.projection(covariantAngle(rotation, i)) 192 | ); 193 | } else { 194 | return Tensor.product( 195 | directionGo, 196 | polarization.zero 197 | ); 198 | } 199 | }) 200 | ) 201 | ); 202 | 203 | // NOTE same notes as for polarizer 204 | export const quarterWavePlate = _.range(4).map((rotation) => 205 | Tensor.sumList( 206 | direction.diode.map((directionGo, i) => 207 | Tensor.product( 208 | directionGo, 209 | polarization.phaseShift( 210 | covariantAngle(rotation, i), 211 | TAU / 4 212 | ) 213 | ) 214 | ) 215 | ) 216 | ); 217 | 218 | // NOTE if I use 'zero' instead of this tensor product, 219 | // 'zero' changes; I am not sure if it is a priblem with sumList or what 220 | export const quarterWavePlateNS = _.range(4).map((rotation) => 221 | Tensor.sumList( 222 | direction.diode.map((directionGo, i) => { 223 | if (i === 1 || i === 3) { 224 | return Tensor.product( 225 | directionGo, 226 | polarization.phaseShift( 227 | covariantAngle(rotation, i), 228 | TAU / 4 229 | ) 230 | ); 231 | } else { 232 | return Tensor.product( 233 | directionGo, 234 | polarization.zero 235 | ); 236 | } 237 | }) 238 | ) 239 | ); 240 | 241 | export const quarterWavePlateWE = _.range(4).map((rotation) => 242 | Tensor.sumList( 243 | direction.diode.map((directionGo, i) => { 244 | if (i === 0 || i === 2) { 245 | return Tensor.product( 246 | directionGo, 247 | polarization.phaseShift( 248 | covariantAngle(rotation, i), 249 | TAU / 4 250 | ) 251 | ); 252 | } else { 253 | return Tensor.product( 254 | directionGo, 255 | polarization.zero 256 | ); 257 | } 258 | }) 259 | ) 260 | ); 261 | 262 | export const faradayRotator = _.range(4).map((rotation) => 263 | Tensor.sum( 264 | Tensor.product( 265 | direction.diode[rotation], 266 | polarization.rotation(TAU / 8) 267 | ), 268 | Tensor.product( 269 | direction.diode[(rotation + 2) % 4], 270 | polarization.rotation(- TAU / 8) 271 | ) 272 | ) 273 | ); 274 | -------------------------------------------------------------------------------- /js/tensor/full.spec.js: -------------------------------------------------------------------------------- 1 | import * as full from './full'; 2 | import _ from 'lodash'; 3 | 4 | function probability(entry) { 5 | return entry.re * entry.re + entry.im * entry.im; 6 | } 7 | 8 | const subspaceAll = ['>-', '>|', '^-', '^|', '<-', '<|', 'v-', 'v|']; 9 | const subspaceDirWE = ['>-', '>|', '<-', '<|']; 10 | const subspaceDirNS = ['^-', '^|', 'v-', 'v|']; 11 | 12 | // calculates norm of a random unit vector within a subspace 13 | function matrixNormOnRandomVector(matrix, subspace = subspaceAll) { 14 | const inputVector = subspace.map((key) => [key, {re: Math.random(), im: Math.random()}]); 15 | const norm = _.sumBy(inputVector, (input) => probability(input[1])); 16 | const outputVector = {}; 17 | let zIn; 18 | inputVector.forEach((input) => { 19 | zIn = input[1]; 20 | matrix.get(input[0]).forEach((zOut, keyOut) => { 21 | if (!_.has(outputVector, keyOut)) { 22 | outputVector[keyOut] = {re: 0, im: 0}; 23 | } 24 | outputVector[keyOut].re += zIn.re * zOut.re - zIn.im * zOut.im; 25 | outputVector[keyOut].im += zIn.re * zOut.im + zIn.im * zOut.re; 26 | }); 27 | }); 28 | 29 | return _(outputVector).values().map(probability).sum() / norm; 30 | } 31 | 32 | 33 | describe('identity', () => { 34 | 35 | it('is unitary', () => { 36 | expect(matrixNormOnRandomVector(full.identity.map)).toBeCloseTo(1, 5); 37 | }); 38 | 39 | }); 40 | 41 | 42 | describe('zero', () => { 43 | 44 | it('absorbs all', () => { 45 | expect(matrixNormOnRandomVector(full.zero.map)).toBeCloseTo(0, 5); 46 | }); 47 | 48 | }); 49 | 50 | 51 | describe('thinMirror', () => { 52 | 53 | it('should consist of 4 tensors', () => { 54 | expect(full.thinMirror.length).toBe(4); 55 | }); 56 | 57 | it('diagonal orientations should consist of unitary tensors', () => { 58 | expect(matrixNormOnRandomVector(full.thinMirror[1].map)).toBeCloseTo(1, 5); 59 | expect(matrixNormOnRandomVector(full.thinMirror[3].map)).toBeCloseTo(1, 5); 60 | }); 61 | 62 | it('| and - orientations should be unitary for perpendicular directions', () => { 63 | 64 | expect(matrixNormOnRandomVector( 65 | full.thinMirror[0].map, subspaceDirNS 66 | )).toBeCloseTo(1, 5); 67 | 68 | expect(matrixNormOnRandomVector( 69 | full.thinMirror[2].map, subspaceDirWE 70 | )).toBeCloseTo(1, 5); 71 | 72 | }); 73 | 74 | }); 75 | 76 | 77 | describe('thinSplitter', () => { 78 | 79 | it('should consist of 4 tensors', () => { 80 | expect(full.thinSplitter.length).toBe(4); 81 | }); 82 | 83 | it('diagonal orientations should consist of unitary tensors', () => { 84 | expect(matrixNormOnRandomVector(full.thinSplitter[1].map)).toBeCloseTo(1, 5); 85 | expect(matrixNormOnRandomVector(full.thinSplitter[3].map)).toBeCloseTo(1, 5); 86 | }); 87 | 88 | it('| and - orientations should be unitary for perpendicular directions', () => { 89 | 90 | expect(matrixNormOnRandomVector( 91 | full.thinSplitter[0].map, subspaceDirNS 92 | )).toBeCloseTo(1, 5); 93 | 94 | expect(matrixNormOnRandomVector( 95 | full.thinSplitter[2].map, subspaceDirWE 96 | )).toBeCloseTo(1, 5); 97 | 98 | }); 99 | 100 | }); 101 | 102 | 103 | describe('thinSplitterCoated', () => { 104 | 105 | it('should consist of 8 tensors', () => { 106 | expect(full.thinSplitterCoated.length).toBe(8); 107 | }); 108 | 109 | it('diagonal orientations should consist of unitary tensors', () => { 110 | expect(matrixNormOnRandomVector(full.thinSplitterCoated[1].map)).toBeCloseTo(1, 5); 111 | expect(matrixNormOnRandomVector(full.thinSplitterCoated[3].map)).toBeCloseTo(1, 5); 112 | expect(matrixNormOnRandomVector(full.thinSplitterCoated[5].map)).toBeCloseTo(1, 5); 113 | expect(matrixNormOnRandomVector(full.thinSplitterCoated[7].map)).toBeCloseTo(1, 5); 114 | }); 115 | 116 | it('| and - orientations should be unitary for perpendicular directions', () => { 117 | 118 | expect(matrixNormOnRandomVector( 119 | full.thinSplitterCoated[0].map, subspaceDirNS 120 | )).toBeCloseTo(1, 5); 121 | 122 | expect(matrixNormOnRandomVector( 123 | full.thinSplitterCoated[2].map, subspaceDirWE 124 | )).toBeCloseTo(1, 5); 125 | 126 | expect(matrixNormOnRandomVector( 127 | full.thinSplitterCoated[4].map, subspaceDirNS 128 | )).toBeCloseTo(1, 5); 129 | 130 | expect(matrixNormOnRandomVector( 131 | full.thinSplitterCoated[6].map, subspaceDirWE 132 | )).toBeCloseTo(1, 5); 133 | 134 | }); 135 | 136 | }); 137 | 138 | 139 | describe('polarizingSplitter', () => { 140 | 141 | it('should consist of 2 tensors', () => { 142 | expect(full.polarizingSplitter.length).toBe(2); 143 | }); 144 | 145 | it('should consist of unitary tensors', () => { 146 | expect(matrixNormOnRandomVector(full.polarizingSplitter[0].map)).toBeCloseTo(1, 5); 147 | expect(matrixNormOnRandomVector(full.polarizingSplitter[1].map)).toBeCloseTo(1, 5); 148 | }); 149 | 150 | }); 151 | 152 | 153 | describe('polarizerNS', () => { 154 | 155 | it('should consist of 4 tensors', () => { 156 | expect(full.polarizerNS.length).toBe(4); 157 | }); 158 | 159 | it('WE directions should be zero', () => { 160 | 161 | full.polarizerNS.forEach((tensor) => { 162 | expect(matrixNormOnRandomVector( 163 | tensor.map, subspaceDirWE 164 | )).toBeCloseTo(0, 5); 165 | }); 166 | 167 | }); 168 | 169 | }); 170 | 171 | 172 | describe('polarizerWE', () => { 173 | 174 | it('should consist of 4 tensors', () => { 175 | expect(full.polarizerWE.length).toBe(4); 176 | }); 177 | 178 | it('NS directions should be zero', () => { 179 | 180 | full.polarizerWE.forEach((tensor) => { 181 | expect(matrixNormOnRandomVector( 182 | tensor.map, subspaceDirNS 183 | )).toBeCloseTo(0, 5); 184 | }); 185 | 186 | }); 187 | 188 | }); 189 | 190 | 191 | describe('quarterWavePlateNS', () => { 192 | 193 | it('should consist of 4 tensors', () => { 194 | expect(full.quarterWavePlateNS.length).toBe(4); 195 | }); 196 | 197 | it('should consist of unitary tensors for NS', () => { 198 | 199 | full.quarterWavePlateNS.forEach((tensor) => { 200 | expect(matrixNormOnRandomVector( 201 | tensor.map, subspaceDirNS 202 | )).toBeCloseTo(1, 5); 203 | }); 204 | 205 | }); 206 | 207 | it('WE directions should be zero', () => { 208 | 209 | full.quarterWavePlateNS.forEach((tensor) => { 210 | expect(matrixNormOnRandomVector( 211 | tensor.map, subspaceDirWE 212 | )).toBeCloseTo(0, 5); 213 | }); 214 | 215 | }); 216 | 217 | }); 218 | 219 | 220 | describe('quarterWavePlateWE', () => { 221 | 222 | it('should consist of 4 tensors', () => { 223 | expect(full.quarterWavePlateWE.length).toBe(4); 224 | }); 225 | 226 | it('should consist of unitary tensors for WE', () => { 227 | 228 | full.quarterWavePlateWE.forEach((tensor) => { 229 | expect(matrixNormOnRandomVector( 230 | tensor.map, subspaceDirWE 231 | )).toBeCloseTo(1, 5); 232 | }); 233 | 234 | }); 235 | 236 | it('NS directions should be zero', () => { 237 | 238 | full.quarterWavePlateWE.forEach((tensor) => { 239 | expect(matrixNormOnRandomVector( 240 | tensor.map, subspaceDirNS 241 | )).toBeCloseTo(0, 5); 242 | }); 243 | 244 | }); 245 | 246 | }); 247 | 248 | 249 | describe('sugarSolution', () => { 250 | 251 | it('should be a unitary tensor', () => { 252 | 253 | expect(matrixNormOnRandomVector( 254 | full.sugarSolution.map 255 | )).toBeCloseTo(1, 5); 256 | 257 | }); 258 | 259 | }); 260 | 261 | 262 | describe('doubleSugarSolution', () => { 263 | 264 | it('should be a unitary tensor', () => { 265 | 266 | expect(matrixNormOnRandomVector( 267 | full.doubleSugarSolution.map 268 | )).toBeCloseTo(1, 5); 269 | 270 | }); 271 | 272 | }); 273 | 274 | 275 | describe('glass', () => { 276 | 277 | it('should be a unitary tensor', () => { 278 | 279 | expect(matrixNormOnRandomVector( 280 | full.glass.map 281 | )).toBeCloseTo(1, 5); 282 | 283 | }); 284 | 285 | }); 286 | 287 | 288 | describe('vacuumJar', () => { 289 | 290 | it('should be a unitary tensor', () => { 291 | 292 | expect(matrixNormOnRandomVector( 293 | full.vacuumJar.map 294 | )).toBeCloseTo(1, 5); 295 | 296 | }); 297 | 298 | }); 299 | 300 | 301 | describe('absorber', () => { 302 | 303 | it('should absorb 50%', () => { 304 | 305 | expect(matrixNormOnRandomVector( 306 | full.absorber.map 307 | )).toBeCloseTo(0.5, 5); 308 | 309 | }); 310 | 311 | }); 312 | 313 | 314 | describe('faradayRotator', () => { 315 | 316 | it('should consist of 4 tensors', () => { 317 | expect(full.faradayRotator.length).toBe(4); 318 | }); 319 | 320 | it('unitary along its orientation', () => { 321 | 322 | expect(matrixNormOnRandomVector( 323 | full.faradayRotator[0].map, subspaceDirWE 324 | )).toBeCloseTo(1, 5); 325 | 326 | expect(matrixNormOnRandomVector( 327 | full.faradayRotator[1].map, subspaceDirNS 328 | )).toBeCloseTo(1, 5); 329 | 330 | expect(matrixNormOnRandomVector( 331 | full.faradayRotator[2].map, subspaceDirWE 332 | )).toBeCloseTo(1, 5); 333 | 334 | expect(matrixNormOnRandomVector( 335 | full.faradayRotator[3].map, subspaceDirNS 336 | )).toBeCloseTo(1, 5); 337 | 338 | }); 339 | 340 | it('absorbing in the perpendicular direction', () => { 341 | 342 | expect(matrixNormOnRandomVector( 343 | full.faradayRotator[0].map, subspaceDirNS 344 | )).toBeCloseTo(0, 5); 345 | 346 | expect(matrixNormOnRandomVector( 347 | full.faradayRotator[1].map, subspaceDirWE 348 | )).toBeCloseTo(0, 5); 349 | 350 | expect(matrixNormOnRandomVector( 351 | full.faradayRotator[2].map, subspaceDirNS 352 | )).toBeCloseTo(0, 5); 353 | 354 | expect(matrixNormOnRandomVector( 355 | full.faradayRotator[3].map, subspaceDirWE 356 | )).toBeCloseTo(0, 5); 357 | 358 | }); 359 | 360 | }); 361 | -------------------------------------------------------------------------------- /js/tensor/polarization.js: -------------------------------------------------------------------------------- 1 | import {Tensor} from './tensor'; 2 | import {TAU} from '../const'; 3 | 4 | export const polarizations = ['-', '|']; 5 | 6 | export const identity = Tensor.fill(polarizations, {re: 1, im: 0}); 7 | export const zero = Tensor.fill(polarizations, {re: 0, im: 0}); 8 | 9 | export const source = Tensor.fromObject({ 10 | '-': {'-': {re: 1, im: 0}}, 11 | }); 12 | 13 | export const reflectPhaseFromLighter = Tensor.fromObject({ 14 | '-': {'-': {re: -1, im: 0}}, 15 | '|': {'|': {re: 1, im: 0}}, 16 | }); 17 | 18 | export const reflectPhaseFromDenser = Tensor.fromObject({ 19 | '-': {'-': {re: 1, im: 0}}, 20 | '|': {'|': {re: -1, im: 0}}, 21 | }); 22 | 23 | /** 24 | * Creates polarization rotation matrix for given angle alpha. 25 | * Sample usage: polarization twister. 26 | */ 27 | // TODO check the sign of rotation 28 | // TODO tests 29 | export const rotation = (alpha) => Tensor.fromObject({ 30 | '-': {'-': {re: Math.cos(alpha), im: 0}, 31 | '|': {re: Math.sin(alpha), im: 0}}, 32 | '|': {'-': {re: -Math.sin(alpha), im: 0}, 33 | '|': {re: Math.cos(alpha), im: 0}}, 34 | }); 35 | 36 | /** 37 | * Creates polarization projection matrix for given angle alpha. 38 | * Sample usage: polarizer. 39 | */ 40 | // TODO tests 41 | export const projection = (alpha) => Tensor.fromObject({ 42 | '-': {'-': {re: Math.cos(alpha) * Math.cos(alpha), im: 0}, 43 | '|': {re: Math.cos(alpha) * Math.sin(alpha), im: 0}}, 44 | '|': {'-': {re: Math.cos(alpha) * Math.sin(alpha), im: 0}, 45 | '|': {re: Math.sin(alpha) * Math.sin(alpha), im: 0}}, 46 | }); 47 | 48 | // note to myself: 49 | // TAU/2 is symmetry 50 | // TAU/4 is rotation to the perpendicular coordinates 51 | 52 | // one gets shifted, second stays the same 53 | // TODO better description 54 | // TODO tests 55 | export const phaseShift = (alpha, phi) => ( 56 | Tensor.sum( 57 | Tensor.byConstant( 58 | projection(alpha), 59 | {re: Math.cos(phi), im: Math.sin(phi)} 60 | ), 61 | projection(alpha + TAU / 4) 62 | ) 63 | ); 64 | 65 | // TODO for the three functions above - invent something to purge almost-zero entries? 66 | 67 | // ones below are NOT polarization-dependent, 68 | // but it might be simpler to keep them there 69 | // or maybe use just tensor.byConstant? 70 | 71 | export const globalPhase = (phi) => Tensor.fill( 72 | polarizations, {re: Math.cos(phi), im: Math.sin(phi)} 73 | ); 74 | 75 | export const globalAbsorption = (transmission) => Tensor.fill( 76 | polarizations, {re: Math.sqrt(transmission), im: 0} 77 | ); 78 | -------------------------------------------------------------------------------- /js/tensor/tensor.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /** 4 | * Tensor - mathematically it corresponds to sparse matrices. 5 | * In JS, it's made of map of maps. 6 | */ 7 | export class Tensor { 8 | constructor(map) { 9 | this.map = map; 10 | } 11 | 12 | static fromObject(object) { 13 | const map = new Map(null); 14 | for (let [key, value] of _.toPairs(object)) { 15 | map.set(key, new Map(_.toPairs(value))); 16 | } 17 | return new Tensor(map); 18 | } 19 | 20 | static product(t1, t2) { 21 | const outerMap = new Map(null); 22 | 23 | for (let [k1, v1] of t1.map) { 24 | for (let [k2, v2] of t2.map) { 25 | const innerMap = new Map(null); 26 | 27 | for (let [i1, w1] of v1) { 28 | for (let [i2, w2] of v2) { 29 | innerMap.set( 30 | `${i1}${i2}`, 31 | { 32 | re: w1.re * w2.re - w1.im * w2.im, 33 | im: w1.re * w2.im + w1.im * w2.re, 34 | } 35 | ); 36 | } 37 | } 38 | 39 | outerMap.set(`${k1}${k2}`, innerMap); 40 | } 41 | } 42 | return new Tensor(outerMap); 43 | } 44 | 45 | product(t) { 46 | return Tensor.product(this, t); 47 | } 48 | 49 | static byConstant(t1, z) { 50 | return Tensor.product(t1, Tensor.fromObject( 51 | {'': {'': {re: z.re, im: z.im}}} 52 | )); 53 | } 54 | 55 | byConstant(z) { 56 | return Tensor.byConstant(this, z); 57 | } 58 | 59 | static sum(t1, t2) { 60 | const outerMap = new Map(null); 61 | const outerKeys = new Set([ 62 | ...t1.map.keys(), 63 | ...t2.map.keys(), 64 | ]); 65 | for (let outerKey of outerKeys) { 66 | const innerMap = new Map(null); 67 | const sourceMaps = _.compact([ 68 | t1.map.get(outerKey), 69 | t2.map.get(outerKey)] 70 | ); 71 | for (let sourceMap of sourceMaps) { 72 | for (let [innerKey, innerValue] of sourceMap) { 73 | if (innerMap.has(innerKey)) { 74 | const existing = innerMap.get(innerKey); 75 | innerValue.re += existing.re; 76 | innerValue.im += existing.im; 77 | } 78 | innerMap.set(innerKey, innerValue); 79 | } 80 | } 81 | outerMap.set(outerKey, innerMap); 82 | } 83 | return new Tensor(outerMap); 84 | } 85 | 86 | static sumList(ts) { 87 | return ts.reduce((acc, t) => Tensor.sum(acc, t)); 88 | } 89 | 90 | sum(t) { 91 | return Tensor.sum(this, t); 92 | } 93 | 94 | static fill(keys, value) { 95 | const outerMap = new Map(null); 96 | for (let key of keys) { 97 | const innerMap = new Map(null); 98 | innerMap.set(key, value); 99 | outerMap.set(key, innerMap); 100 | } 101 | return new Tensor(outerMap); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /js/tensor/tensor.spec.js: -------------------------------------------------------------------------------- 1 | import {Tensor} from './tensor'; 2 | 3 | describe('Tensor', () => { 4 | it('should create tensors from objects', () => { 5 | const obj = {A: {A: {re: 1, im: 0}}}; 6 | const t = Tensor.fromObject(obj); 7 | expect(t instanceof Tensor).toBe(true); 8 | expect(t.map instanceof Map).toBe(true); 9 | expect(t.map.get('A') instanceof Map).toBe(true); 10 | expect(t.map.get('A').get('A')).toEqual({re: 1, im: 0}); 11 | }); 12 | }); 13 | 14 | describe('Tensor.product', () => { 15 | it('should multiply sparse matrices', () => { 16 | const first = Tensor.fromObject({ 17 | A: { 18 | A: {re: 1, im: 0}, 19 | B: {re: 2, im: 3}, 20 | }, 21 | B: { 22 | B: {re: -1, im: 0}, 23 | C: {re: 2, im: 3}, 24 | }, 25 | }); 26 | const second = Tensor.fromObject({ 27 | a: { 28 | a: {re: 1, im: 0}, 29 | b: {re: 2, im: 3}, 30 | }, 31 | b: { 32 | a: {re: -1, im: 0}, 33 | b: {re: 2, im: 3}, 34 | }, 35 | }); 36 | const product = Tensor.fromObject({ 37 | Aa: { 38 | Aa: {re: 1, im: 0}, 39 | Ab: {re: 2, im: 3}, 40 | Ba: {re: 2, im: 3}, 41 | Bb: {re: -5, im: 12}, 42 | }, 43 | Ab: { 44 | Aa: {re: -1, im: 0}, 45 | Ab: {re: 2, im: 3}, 46 | Ba: {re: -2, im: -3}, 47 | Bb: {re: -5, im: 12}, 48 | }, 49 | Ba: { 50 | Aa: {re: -1, im: 0}, 51 | Ab: {re: -2, im: -3}, 52 | Ba: {re: 2, im: 3}, 53 | Bb: {re: -5, im: 12}, 54 | }, 55 | Bb: { 56 | Aa: {re: 1, im: -0}, 57 | Ab: {re: -2, im: -3}, 58 | Ba: {re: -2, im: -3}, 59 | Bb: {re: -5, im: 12}, 60 | }, 61 | }); 62 | expect(Tensor.product(first, second)).toEqual(product); 63 | expect(first.product(second)).toEqual(product); 64 | }); 65 | }); 66 | 67 | describe('Tensor.byConstant', () => { 68 | it('should multiply matrix by constant', () => { 69 | const matrix = Tensor.fromObject({ 70 | A: { 71 | A: {re: 1, im: 0}, 72 | B: {re: 2, im: 3}, 73 | }, 74 | B: { 75 | B: {re: -1, im: 0}, 76 | C: {re: 2, im: 3}, 77 | }, 78 | }); 79 | const factor = {re: 1, im: 1}; 80 | const product = Tensor.fromObject({ 81 | A: { 82 | A: {re: 1, im: 1}, 83 | B: {re: -1, im: 5}, 84 | }, 85 | B: { 86 | B: {re: -1, im: -1}, 87 | C: {re: 5, im: -1}, 88 | }, 89 | }); 90 | expect(Tensor.byConstant(matrix, factor)).toEqual(product); 91 | expect(matrix.byConstant(factor)).toEqual(product); 92 | }); 93 | }); 94 | 95 | describe('Tensor.sum', () => { 96 | it('should add sparse matrices', () => { 97 | const first = Tensor.fromObject({ 98 | A: { 99 | A: {re: 1, im: 0}, 100 | B: {re: 2, im: 3}, 101 | }, 102 | B: { 103 | B: {re: -1, im: 0}, 104 | C: {re: 2, im: 3}, 105 | }, 106 | }); 107 | const second = Tensor.fromObject({ 108 | B: { 109 | A: {re: 1, im: 0}, 110 | B: {re: 2, im: 3}, 111 | }, 112 | C: { 113 | B: {re: -1, im: 0}, 114 | }, 115 | }); 116 | const sum = Tensor.fromObject({ 117 | A: { 118 | A: {re: 1, im: 0}, 119 | B: {re: 2, im: 3}, 120 | }, 121 | B: { 122 | A: {re: 1, im: 0}, 123 | B: {re: 1, im: 3}, 124 | C: {re: 2, im: 3}, 125 | }, 126 | C: { 127 | B: {re: -1, im: 0}, 128 | }, 129 | }); 130 | expect(Tensor.sum(first, second)).toEqual(sum); 131 | expect(first.sum(second)).toEqual(sum); 132 | }); 133 | }); 134 | 135 | describe('Tensor.fill', () => { 136 | it('should fill matrix with a value', () => { 137 | const value = {re: 1, im: 0}; 138 | const keys = ['A', 'B', 'C']; 139 | const filled = Tensor.fromObject({ 140 | A: {A: value}, 141 | B: {B: value}, 142 | C: {C: value}, 143 | }); 144 | expect(Tensor.fill(keys, value)).toEqual(filled); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /js/test_utils/mock_d3.js: -------------------------------------------------------------------------------- 1 | // Very simple mock of a d3 selection. 2 | // It has some empty methods that are chainable. 3 | export class MockD3 { 4 | append() { 5 | return new MockD3(); 6 | } 7 | attr() { 8 | return this; 9 | } 10 | classed() { 11 | return this; 12 | } 13 | remove() { 14 | return this; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /js/tile.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import * as config from './config'; 4 | import * as full from './tensor/full'; 5 | import {SoundService} from './sound_service'; 6 | 7 | const pascalCase = (str) => 8 | str.charAt(0).toUpperCase() + _.camelCase(str.slice(1)); 9 | 10 | export const Vacuum = { 11 | svgName: 'vacuum', 12 | desc: { 13 | name: 'Nothing (except for some air)', 14 | flavour: '', 15 | summary: 'Visible light is only 0.03% slower in the air than in the vacuum.', 16 | }, 17 | maxRotation: 1, 18 | rotationAngle: 0, 19 | transition: () => full.identity, 20 | }; 21 | 22 | export const Source = { 23 | svgName: 'source', 24 | desc: { 25 | name: 'Single Photon Source', 26 | flavour: 'a\u2020 - an excitation, raise from the vacuum!', 27 | summary: 'An on-demand single photon source. (CLICK to EMIT!)', 28 | }, 29 | maxRotation: 4, // > ^ < v 30 | rotationAngle: 90, 31 | transition: () => full.zero, 32 | generation: (rotation) => full.source[rotation], 33 | }; 34 | 35 | // maybe will be changed to a typical, one-side corner sube 36 | export const CornerCube = { 37 | svgName: 'corner-cube', 38 | desc: { 39 | name: 'Corner Cube', 40 | flavour: 'Like a mirror but rotating, not - reflecting', 41 | summary: 'Three perpendicular reflective planes make the reflecting going the same way. Also, they save lives on the streets.', 42 | }, 43 | maxRotation: 1, 44 | rotationAngle: 0, 45 | transition: () => full.cornerCube, 46 | }; 47 | 48 | export const ThinMirror = { 49 | svgName: 'thin-mirror', 50 | desc: { 51 | name: 'Mirror', 52 | flavour: 'Making photons in two places at once and binding them again.', 53 | summary: 'Metallic or dielectric mirror.', 54 | }, 55 | maxRotation: 4, // - / | \ 56 | rotationAngle: 45, 57 | transition: (rotation) => full.thinMirror[rotation], 58 | }; 59 | 60 | // most likely it will fo as "BeamSplitter" 61 | export const ThinSplitter = { 62 | svgName: 'thin-splitter', 63 | desc: { 64 | name: '50/50 Beam Splitter', 65 | flavour: 'A thin slice of glass does amazing things!', 66 | summary: 'A thin slab of glass reflecting half the beam, and transmitting other half of it.', 67 | }, 68 | maxRotation: 4, // - / | \ 69 | rotationAngle: 45, 70 | transition: (rotation) => full.thinSplitter[rotation], 71 | }; 72 | 73 | export const ThinSplitterCoated = { 74 | svgName: 'thin-splitter-coated', 75 | desc: { 76 | name: 'Coated 50/50 Beam Splitter', 77 | flavour: 'Like a bread slice with butter', 78 | summary: 'A thin slab of glass with a reflective layer - reflecting half the beam and transmitting the other half of it.', 79 | }, 80 | maxRotation: 8, // - / | \ - / | \ 81 | rotationAngle: 45, 82 | transition: (rotation) => full.thinSplitterCoated[rotation], 83 | }; 84 | 85 | export const PolarizingSplitter = { 86 | svgName: 'polarizing-splitter', 87 | desc: { 88 | name: 'Polarizing Beam Splitter', 89 | flavour: '', 90 | summary: 'Reflects vertical polarization (↕), transmits horizonal polarization (↔).', 91 | }, 92 | maxRotation: 2, // / \ 93 | rotationAngle: 90, 94 | transition: (rotation) => full.polarizingSplitter[rotation], 95 | }; 96 | 97 | // deprecated 98 | export const Polarizer = { 99 | svgName: 'polarizer', 100 | desc: { 101 | name: 'Absorptive Polarizer', 102 | flavour: '', 103 | summary: 'Anisotropic polymer strands capture electric oscillations parallel to them. Used in photography.', 104 | }, 105 | maxRotation: 4, // - / | \ 106 | rotationAngle: 45, 107 | transition: (rotation) => full.polarizer[rotation], 108 | drawUnrotablePart: (that) => { 109 | that.g.append('line') 110 | .attr('class', 'wire') 111 | .attr('x1', 25 / Math.sqrt(2)) 112 | .attr('x2', 35) 113 | .attr('y1', 25 / Math.sqrt(2)) 114 | .attr('y2', 35); 115 | }, 116 | }; 117 | 118 | export const PolarizerNS = { 119 | svgName: 'polarizer-n-s', 120 | desc: { 121 | name: 'Absorptive Polarizer (North-South)', 122 | flavour: '', 123 | summary: 'Anisotropic polymer strands capture electric oscillations parallel to them. Used in photography.', 124 | }, 125 | maxRotation: 4, // - / | \ 126 | rotationAngle: 45, 127 | transition: (rotation) => full.polarizerNS[rotation], 128 | drawUnrotablePart: (that) => { 129 | that.g.append('path') 130 | .attr('class', 'metal-edge polarizer-side') 131 | .attr('d', 'M -25 0 v 10 a 25 25 0 0 0 50 0 v -10 a 25 25 0 0 1 -50 0'); 132 | }, 133 | }; 134 | 135 | export const PolarizerWE = { 136 | svgName: 'polarizer-w-e', 137 | desc: { 138 | name: 'Absorptive Polarizer (West-East)', 139 | flavour: '', 140 | summary: 'Anisotropic polymer strands capture electric oscillations parallel to them. Used in photography.', 141 | }, 142 | maxRotation: 4, // - / | \ 143 | rotationAngle: 45, 144 | transition: (rotation) => full.polarizerWE[rotation], 145 | drawUnrotablePart: (that) => { 146 | that.g.append('path') 147 | .attr('class', 'metal-edge polarizer-side') 148 | .attr('d', 'M 0 -25 h 10 a 25 25 0 0 1 0 50 h -10 a 25 25 0 0 0 0 -50'); 149 | }, 150 | }; 151 | 152 | // deprecated 153 | export const QuarterWavePlate = { 154 | svgName: 'quarter-wave-plate', 155 | desc: { 156 | name: 'Quarter Wave Plate', 157 | flavour: '', 158 | summary: 'It delays one polarization (with darker lines) by \u03BB/4. When applied correctly, it can change linear polarization into circular, and vice versa.', 159 | }, 160 | maxRotation: 4, // - / | \ 161 | rotationAngle: 45, 162 | transition: (rotation) => full.quarterWavePlate[rotation], 163 | }; 164 | 165 | export const QuarterWavePlateNS = { 166 | svgName: 'quarter-wave-plate-n-s', 167 | desc: { 168 | name: 'Quarter Wave Plate (North-South)', 169 | flavour: '', 170 | summary: 'It delays one polarization (with darker lines) by \u03BB/4. When applied correctly, it can change linear polarization into circular, and vice versa.', 171 | }, 172 | maxRotation: 4, // - / | \ 173 | rotationAngle: 45, 174 | transition: (rotation) => full.quarterWavePlateNS[rotation], 175 | drawUnrotablePart: (that) => { 176 | that.g.append('path') 177 | .attr('class', 'glass-edge glass') 178 | .attr('d', 'M -25 10 v 10 l 15 15 h 20 l 15 -15 v -10 l -15 15 h -20 z'); 179 | }, 180 | }; 181 | 182 | export const QuarterWavePlateWE = { 183 | svgName: 'quarter-wave-plate-w-e', 184 | desc: { 185 | name: 'Quarter Wave Plate (West-East)', 186 | flavour: '', 187 | summary: 'It delays one polarization (with darker lines) by \u03BB/4. When applied correctly, it can change linear polarization into circular, and vice versa.', 188 | }, 189 | maxRotation: 4, // - / | \ 190 | rotationAngle: 45, 191 | transition: (rotation) => full.quarterWavePlateWE[rotation], 192 | drawUnrotablePart: (that) => { 193 | that.g.append('path') 194 | .attr('class', 'glass-edge glass') 195 | .attr('d', 'M 10 -25 h 10 l 15 15 v 20 l -15 15 h -10 l 15 -15 v -20 z'); 196 | }, 197 | }; 198 | 199 | export const SugarSolution = { 200 | svgName: 'sugar-solution', 201 | desc: { 202 | name: 'Sugar Solution', 203 | flavour: 'Vodka is a solution. But Sugar Solution is the light-twisting solution.', 204 | summary: 'Table sugar is a chiral molecule – it does not look the same as its mirror reflection. We put it in an amount, so it rotates polarization by 45\u00B0.', 205 | }, 206 | maxRotation: 1, // [] 207 | rotationAngle: 360, 208 | transition: () => full.sugarSolution, 209 | }; 210 | 211 | export const DoubleSugarSolution = { 212 | svgName: 'double-sugar-solution', 213 | desc: { 214 | name: 'Double Sugar Solution', 215 | flavour: 'Vodka is a solution. But Sugar Solution is the light-twisting solution.', 216 | summary: 'Table sugar is a chiral molecule – it does not look the same as its mirror reflection. It is the American version - more straws, more sugar, so it rotates polarization by 90\u00B0.', 217 | }, 218 | maxRotation: 1, // [] 219 | rotationAngle: 360, 220 | transition: () => full.doubleSugarSolution, 221 | }; 222 | 223 | export const Mine = { 224 | svgName: 'mine', 225 | desc: { 226 | name: 'Light-Sensitive Bomb', 227 | flavour: 'If it does NOT click, you will have sunglasses… and a pair of hands.', 228 | summary: 'Once it absorbs a single photon, it sets off.', 229 | }, 230 | maxRotation: 1, // [] 231 | rotationAngle: 360, 232 | transition: () => full.zero, 233 | absorbSound: () => { 234 | SoundService.play('mine'); 235 | }, 236 | absorbAnimation: (that) => { 237 | 238 | const gDom = that.g[0][0]; 239 | gDom.parentNode.appendChild(gDom); 240 | 241 | that.g.select('.element') 242 | .style('opacity', 0) 243 | .transition() 244 | .delay(config.absorptionDuration / 3) 245 | .duration(config.absorptionDuration) 246 | .style('opacity', 1); 247 | 248 | that.g.append('use') 249 | .attr('xlink:href', '#mine-absorbed') 250 | .attr('transform', 'scale(0.1)') 251 | .transition() 252 | .duration(config.absorptionDuration / 3) 253 | .ease('linear') 254 | .attr('transform', 'scale(100)') 255 | .style('opacity', 0) 256 | .remove(); 257 | }, 258 | }; 259 | 260 | // or a brick? 261 | export const Rock = { 262 | svgName: 'rock', 263 | desc: { 264 | name: 'Rock', 265 | flavour: 'Every rock has a life, has a spirit, has a name!', 266 | summary: 'Dark and immersive as your sweetheart\'s depth of eyes. Absorbs light. And is sensitive.', 267 | }, 268 | maxRotation: 1, // [] 269 | rotationAngle: 360, 270 | transition: () => full.zero, 271 | absorbSound: () => { 272 | SoundService.play('rock'); 273 | }, 274 | absorbAnimation: (that) => { 275 | const r = 7; 276 | that.g.append('rect') 277 | .attr('x', -10 - r) 278 | .attr('y', -10 - r) 279 | .attr('width', 2 * r) 280 | .attr('height', 0) 281 | .style('fill', 'black') 282 | .transition() 283 | .ease('linear') 284 | .duration(0.2 * config.absorptionDuration) 285 | .attr('height', 2 * r) 286 | .transition() 287 | .delay(0.2 * config.absorptionDuration) 288 | .duration(0.8 * config.absorptionDuration) 289 | .attr('height', 0) 290 | .remove(); 291 | 292 | that.g.append('rect') 293 | .attr('x', 5 - r) 294 | .attr('y', -5 - r) 295 | .attr('width', 2 * r) 296 | .attr('height', 0) 297 | .style('fill', 'black') 298 | .transition() 299 | .ease('linear') 300 | .duration(0.2 * config.absorptionDuration) 301 | .attr('height', 2 * r) 302 | .transition() 303 | .delay(0.2 * config.absorptionDuration) 304 | .duration(0.8 * config.absorptionDuration) 305 | .attr('height', 0) 306 | .remove(); 307 | }, 308 | }; 309 | 310 | export const Glass = { 311 | svgName: 'glass', 312 | desc: { 313 | name: 'Glass Slab', 314 | flavour: '', 315 | summary: 'Higher refractive index makes light slower. We set its thickness so it retards the phase by \u03BB/4. Useful for changing interference.', 316 | }, 317 | maxRotation: 1, // [] 318 | rotationAngle: 360, 319 | transition: () => full.glass, 320 | }; 321 | 322 | export const VacuumJar = { 323 | svgName: 'vacuum-jar', 324 | desc: { 325 | name: 'Vacuum Jar', 326 | flavour: 'Pure timespace without relativistic energy density. Served in a bottle.', 327 | summary: 'Even air retards light a bit. We set the thickness of vacuum so it advances the phase by \u03BB/4. Useful for changing interference.', 328 | }, 329 | maxRotation: 1, // [] 330 | rotationAngle: 360, 331 | transition: () => full.vacuumJar, 332 | }; 333 | 334 | export const Absorber = { 335 | svgName: 'absorber', 336 | desc: { 337 | name: 'Absorber / Neutral-Density Filter', 338 | flavour: 'To click or not to click?', 339 | summary: 'Filter with 50% absorption probability.', 340 | }, 341 | maxRotation: 1, // [] 342 | rotationAngle: 360, 343 | transition: () => full.absorber, 344 | absorbSound: () => { 345 | SoundService.play('absorber'); 346 | }, 347 | }; 348 | 349 | export const Detector = { 350 | svgName: 'detector', 351 | desc: { 352 | name: 'Photon Detector', 353 | flavour: '', 354 | summary: 'Detects and amplifies electric signal from each single photon, from a single direction. Your goal is to get photon there!', 355 | }, 356 | maxRotation: 4, // > ^ < v 357 | rotationAngle: 90, 358 | transition: (rotation) => full.detector[rotation], 359 | absorbSound: () => { 360 | SoundService.play('detector'); 361 | }, 362 | absorbAnimation: (that) => { 363 | 364 | // maybe until element move or next run? 365 | that.g.append('use') 366 | .attr('xlink:href', '#detector-excitation') 367 | .attr('class', 'absorbed') 368 | .attr('transform', `rotate(${-that.type.rotationAngle * that.rotation},0,0)`) 369 | .transition() 370 | .delay(config.absorptionDuration * 2) 371 | .duration(config.absorptionDuration * 3) 372 | .style('opacity', 0) 373 | .remove(); 374 | 375 | that.g.append('use') 376 | .attr('xlink:href', '#detector-excitation') 377 | .attr('transform', 'scale(1)') 378 | .transition() 379 | .duration(config.absorptionDuration / 3) 380 | .ease('linear') 381 | .attr('transform', 'scale(20)') 382 | .style('opacity', 0) 383 | .remove(); 384 | 385 | }, 386 | }; 387 | 388 | export const DetectorFour = { 389 | svgName: 'detector-four', 390 | desc: { 391 | name: 'Omnidirectional Photon Detector', 392 | flavour: '', 393 | summary: 'Detects and amplifies electric signal from each single photon, from all directions. Typically, it is the goal to get the photon here.', 394 | }, 395 | maxRotation: 1, // [] 396 | rotationAngle: 360, 397 | transition: () => full.zero, 398 | absorbSound: () => { 399 | SoundService.play('detector'); 400 | }, 401 | absorbAnimation: (that) => { 402 | 403 | // maybe until element move or next run? 404 | that.g.append('use') 405 | .attr('xlink:href', '#detector-excitation') 406 | .attr('class', 'absorbed') 407 | .attr('transform', `rotate(${-that.type.rotationAngle * that.rotation},0,0)`) 408 | .transition() 409 | .delay(config.absorptionDuration * 2) 410 | .duration(config.absorptionDuration * 3) 411 | .style('opacity', 0) 412 | .remove(); 413 | 414 | that.g.append('use') 415 | .attr('xlink:href', '#detector-excitation') 416 | .attr('transform', 'scale(1)') 417 | .transition() 418 | .duration(config.absorptionDuration / 3) 419 | .ease('linear') 420 | .attr('transform', 'scale(20)') 421 | .style('opacity', 0) 422 | .remove(); 423 | 424 | }, 425 | }; 426 | 427 | export const FaradayRotator = { 428 | svgName: 'faraday-rotator', 429 | desc: { 430 | name: 'Faraday Rotator', 431 | flavour: 'You can go back, but it won\'t be the same.', 432 | summary: 'Rotates polarization with magnetic field by 45\u00B0. Has different symmetries than Sugar Solution. A building block for optical diodes.', 433 | }, 434 | maxRotation: 4, // > ^ < v 435 | rotationAngle: 90, 436 | transition: (rotation) => full.faradayRotator[rotation], 437 | }; 438 | 439 | export class Tile { 440 | constructor(type = Vacuum, rotation = 0, frozen = true, i = 0, j = 0) { 441 | this.type = type; 442 | this.rotation = rotation; 443 | this.frozen = frozen; 444 | this.i = i; 445 | this.j = j; 446 | // this.g // d3 group selector in which it is 447 | } 448 | 449 | draw() { 450 | 451 | if (this.type.drawUnrotablePart !== undefined) { 452 | this.type.drawUnrotablePart(this); 453 | } 454 | 455 | this.g.append('use') 456 | .attr('xlink:href', () => `#${this.type.svgName}`) 457 | .attr('class', 'element') 458 | .attr('transform', () => `rotate(${-this.type.rotationAngle * this.rotation},0,0)`); 459 | 460 | } 461 | 462 | rotate() { 463 | 464 | const element = this.g.select('.element'); 465 | this.rotation = (this.rotation + 1) % this.type.maxRotation; 466 | 467 | // Assure that rotation animation is clockwise 468 | const startAngle = this.type.rotationAngle * (this.rotation - 1); 469 | element 470 | .attr('transform', `rotate(${-startAngle},0,0)`); 471 | 472 | // Rotation animation 473 | const endAngle = this.type.rotationAngle * this.rotation; 474 | element 475 | .transition() 476 | .duration(config.rotationSpeed) 477 | .attr('transform', `rotate(${-endAngle},0,0)`); 478 | 479 | } 480 | 481 | absorbSound() { 482 | (this.type.absorbSound || _.noop)(); 483 | } 484 | 485 | absorbAnimation() { 486 | 487 | // NOTE or maybe just class inheritance? 488 | if (this.type.absorbAnimation != null) { 489 | this.type.absorbAnimation(this); 490 | } else { 491 | this.g.select('.element') 492 | .style('opacity', 0.3) 493 | .transition() 494 | .duration(config.absorptionDuration) 495 | .style('opacity', 1); 496 | } 497 | 498 | } 499 | 500 | get x() { 501 | return config.tileSize * this.i; 502 | } 503 | 504 | get y() { 505 | return config.tileSize * this.j; 506 | } 507 | 508 | get transitionAmplitudes() { 509 | return this.type.transition(this.rotation); 510 | } 511 | 512 | get tileName() { 513 | return pascalCase(this.type.svgName); 514 | } 515 | 516 | get isDetector() { 517 | return this.tileName === 'Detector' || this.tileName === 'DetectorFour'; 518 | } 519 | } 520 | 521 | export const allTiles = [ 522 | 'Vacuum', 523 | 'Source', 524 | 'CornerCube', 525 | 'ThinMirror', 526 | 'ThinSplitter', 527 | 'ThinSplitterCoated', 528 | 'PolarizingSplitter', 529 | 'PolarizerNS', 530 | 'PolarizerWE', 531 | 'QuarterWavePlateNS', 532 | 'QuarterWavePlateWE', 533 | 'SugarSolution', 534 | 'DoubleSugarSolution', 535 | 'Mine', 536 | 'Rock', 537 | 'Glass', 538 | 'VacuumJar', 539 | 'Absorber', 540 | 'Detector', 541 | 'DetectorFour', 542 | 'FaradayRotator', 543 | ]; 544 | 545 | export const nonVacuumTiles = _.without(allTiles, 'Vacuum'); 546 | -------------------------------------------------------------------------------- /js/tile_helper.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import {tileSize, tileHelperWidth, tileHelperHeight} from './config'; 3 | 4 | // shamelessly stolen from https://bl.ocks.org/mbostock/7555321 5 | const wrap = (text, width) => { 6 | text.each(function() { 7 | const text = d3.select(this); 8 | const words = text.text().split(/\s+/).reverse(); 9 | let word; 10 | let line = []; 11 | let lineNumber = 0; 12 | const lineHeight = 1.1; // ems 13 | const x = text.attr('x') || 0; 14 | const y = text.attr('y') || 0; 15 | const dy = parseFloat(text.attr('dy')) || 0; 16 | let tspan = text.text(null).append('tspan') 17 | .attr('x', x) 18 | .attr('y', y) 19 | .attr('dy', dy + 'em'); 20 | while (word = words.pop()) { 21 | line.push(word); 22 | tspan.text(line.join(' ')); 23 | if (tspan.node().getComputedTextLength() > width) { 24 | line.pop(); 25 | tspan.text(line.join(' ')); 26 | line = [word]; 27 | tspan = text.append('tspan') 28 | .attr('x', x) 29 | .attr('y', y) 30 | .attr('dy', ++lineNumber * lineHeight + dy + 'em') 31 | .text(word); 32 | } 33 | } 34 | }); 35 | } 36 | 37 | export class TileHelper { 38 | constructor(svg, bareBoard, game) { 39 | this.svg = svg; 40 | this.game = game; 41 | this.width = tileHelperWidth * tileSize; 42 | this.height = tileHelperHeight * tileSize; 43 | 44 | // NOTE this is a bit problematic as it depends on the level 45 | this.shiftX = (bareBoard.level.width + 1) * tileSize; 46 | const marginForAnimationControls = 2.5; 47 | this.shiftY = (bareBoard.level.height 48 | - tileHelperHeight 49 | - marginForAnimationControls) * tileSize; 50 | this.initialDraw(); 51 | } 52 | 53 | initialDraw() { 54 | 55 | // Reset element 56 | this.svg.select('.helper').remove(); 57 | 58 | // Create 59 | this.helperGroup = this.svg 60 | .append('g') 61 | .attr('class', 'helper') 62 | .attr('transform', `translate(${this.shiftX},${this.shiftY})`); 63 | 64 | this.helperGroup.append('rect') 65 | .attr('class', 'svg-interface-box-stroke') 66 | .attr('width', `${this.width}`) 67 | .attr('height', `${this.height}`); 68 | 69 | this.tileBackground = this.helperGroup.append('rect') 70 | .attr('class', 'background-tile') 71 | .attr('x', '1') 72 | .attr('y', '1') 73 | .attr('width', '98') 74 | .attr('height', '98'); 75 | 76 | this.tileUse = this.helperGroup.append('use') 77 | .attr('class', 'element helper-element') 78 | .attr('x', tileSize / 2) 79 | .attr('y', tileSize / 2); 80 | 81 | this.tileName = this.helperGroup.append('text') 82 | .attr('class', 'helper-name unselectable') 83 | .attr('x', 2.25 * tileSize) 84 | .attr('y', tileSize * 0.4); // 0.5 factor might be too much for 3 lines 85 | 86 | this.tileSummmary = this.helperGroup.append('text') 87 | .attr('class', 'helper-summary unselectable') 88 | .attr('x', 0.25 * tileSize) 89 | .attr('y', 1.5 * tileSize); 90 | 91 | this.helperHitbox = this.helperGroup.append('rect') 92 | .attr('class', 'helper-hitbox') 93 | .attr('width', `${this.width}`) 94 | .attr('height', `${this.height}`); 95 | 96 | } 97 | 98 | show(tile) { 99 | 100 | this.helperHitbox.on('click', () => { 101 | this.game.setEncyclopediaItem(tile.tileName); 102 | this.game.setView('encyclopediaItem'); 103 | }); 104 | this.tileUse.attr('xlink:href', `#${tile.type.svgName}`) 105 | this.tileName.text(tile.type.desc.name) 106 | .call(wrap, (tileHelperWidth - 2) * tileSize); 107 | this.tileSummmary.text(tile.type.desc.summary) 108 | .call(wrap, (tileHelperWidth - 0.5) * tileSize); 109 | 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /js/title_manager.js: -------------------------------------------------------------------------------- 1 | /*global window:false*/ 2 | import {displayMessageTimeout} from './config'; 3 | 4 | // TODO(migdal): passing that many selectors is nasty - refactor 5 | export class TitleManager { 6 | constructor(titleBar, subtitleElem, blinkSvg) { 7 | this.titleBar = titleBar; 8 | this.titleElem = titleBar.select('.title-text'); 9 | this.levelNumberElem = titleBar.select('.level-number'); 10 | this.blinkSvg = blinkSvg; 11 | 12 | this.subtitleElem = subtitleElem; 13 | this.messageElem = this.subtitleElem.select('.subtitle-message'); 14 | this.defaultMessage = ''; 15 | } 16 | 17 | setTitle(title) { 18 | this.titleElem.html(title); 19 | } 20 | 21 | setLevelNumber(levelNumber) { 22 | this.levelNumberElem.html(levelNumber); 23 | } 24 | 25 | setDefaultMessage(message, type) { 26 | this.messageElem.interrupt(); 27 | this.defaultMessage = message; 28 | this.displayMessage(message, type, -1); 29 | } 30 | 31 | displayMessage(message, type, timeout = displayMessageTimeout) { 32 | this.messageElem.interrupt().style('opacity', 1); 33 | this.messageElem 34 | .text(message) 35 | .classed('message-success', type === 'success') 36 | .classed('message-failure', type === 'failure') 37 | .classed('message-progress', type === 'progress'); 38 | if (timeout > 0) { 39 | this.messageElem.transition().duration(displayMessageTimeout) 40 | .style('opacity', 0) 41 | .delay(displayMessageTimeout) 42 | .style('opacity', 1) 43 | .text(this.defaultMessage); 44 | } 45 | } 46 | 47 | activateNextLevelButton(nextLevelCallback) { 48 | const titleBar = this.titleBar; 49 | titleBar.select('.next-level') 50 | .on('click', nextLevelCallback); 51 | } 52 | 53 | showNextLevelButton(ifShow) { 54 | // Show next level button? 55 | this.titleBar.select('.next-level').classed('hidden', !ifShow); 56 | this.blinkSvg.classed('hidden', !ifShow); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /js/tooltip.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | 3 | export class Tooltip { 4 | 5 | constructor(selector) { 6 | this.tooltip = selector 7 | .append('div') 8 | .attr('class', 'tooltip') 9 | .style('opacity', 0); 10 | } 11 | 12 | show(html) { 13 | this.tooltip.style('opacity', 0.8) 14 | .style('left', (d3.event.pageX + 15) + 'px') 15 | .style('top', (d3.event.pageY + 8) + 'px') 16 | .html(html); 17 | } 18 | 19 | out() { 20 | this.tooltip 21 | .style('opacity', 0); 22 | } 23 | 24 | destroy() { 25 | this.tooltip.remove(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /js/transition_heatmap.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import _ from 'lodash'; 3 | import {TAU, EPSILON} from './const'; 4 | import {Tooltip} from './tooltip'; 5 | 6 | const toggleDuraton = 1000; 7 | 8 | const complexToPureColor = (z) => { 9 | if (z.re === 0 && z.im === 0) { 10 | return '#ffffff'; 11 | } else { 12 | const angleInDegrees = (Math.atan2(z.im, z.re) * 360 / TAU + 360) % 360; 13 | // NOTE for color (light theme) it would be: d3.hsl(angleInDegrees, 1, 1 - r / 2) 14 | return d3.hsl(angleInDegrees, 1, 0.5).toString(); 15 | } 16 | }; 17 | 18 | const complexToOpacity = (z) => Math.sqrt(z.re * z.re + z.im * z.im); 19 | 20 | // see http://www.fileformat.info/info/unicode/block/arrows/utf8test.htm 21 | const prettierArrows = { 22 | '>': '\u21e2', // ⇢ 23 | '^': '\u21e1', // ⇡ 24 | '<': '\u21e0', // ⇠ 25 | 'v': '\u21e3', // ⇣ 26 | '-': '\u2194', // ↔ 27 | '|': '\u2195', // ↕ 28 | }; 29 | 30 | const prettifyBasis = (basis) => `${prettierArrows[basis[0]]}${prettierArrows[basis[1]]}`; 31 | 32 | const basisDirPol = ['>-', '>|', '^-', '^|', '<-', '<|', 'v-', 'v|']; 33 | const basisPolDir = ['>-', '^-', '<-', 'v-', '>|', '^|', '<|', 'v|']; 34 | 35 | export class TransitionHeatmap { 36 | constructor(selectorSvg, selectorForTooltip, size=200) { 37 | this.g = selectorSvg.append('g') 38 | .attr('class', 'transition-heatmap') 39 | .on('click', () => this.toggleBasis()); 40 | 41 | this.tooltip = new Tooltip(selectorForTooltip); 42 | this.size = size; 43 | this.basis = basisDirPol; 44 | } 45 | 46 | updateFromTensor(tensor) { 47 | 48 | const arrayContent = this.basis 49 | .map((outputBase) => this.basis 50 | .map((inputBase) => { 51 | const element = tensor.get(inputBase).get(outputBase) || {re: 0, im: 0}; 52 | return { 53 | from: inputBase, 54 | to: outputBase, 55 | re: element.re, 56 | im: element.im, 57 | }; 58 | }) 59 | ); 60 | 61 | this.update(this.basis, _.flatten(arrayContent)); 62 | } 63 | 64 | toggleBasis() { 65 | 66 | if (this.basis === basisDirPol) { 67 | this.basis = basisPolDir; 68 | } else { 69 | this.basis = basisDirPol; 70 | } 71 | 72 | this.update(this.basis); 73 | 74 | } 75 | 76 | update(labels, matrixElements=null) { 77 | 78 | const position = _.fromPairs(labels.map((d, i) => [d, i])); 79 | 80 | const scale = d3.scale.linear() 81 | .domain([-1, labels.length]) 82 | .range([0, this.size]); 83 | 84 | const squareSize = scale(1) - scale(0); 85 | 86 | // in (top) basis labels 87 | 88 | this.labelIn = this.g 89 | .selectAll('.label-in') 90 | .data(labels, (d) => d); 91 | 92 | this.labelIn.enter() 93 | .append('text') 94 | .attr('class', 'label-in'); 95 | 96 | this.labelIn 97 | .attr('y', scale(-0.5)) 98 | .style('text-anchor', 'middle') 99 | .text(prettifyBasis) 100 | .transition() 101 | .duration(toggleDuraton) 102 | .attr('x', (d, i) => scale(i + 0.5)) 103 | .attr('dy', '0.5em'); 104 | 105 | this.labelIn.exit() 106 | .remove(); 107 | 108 | // out (left) basis labels 109 | 110 | this.labelOut = this.g 111 | .selectAll('.label-out') 112 | .data(labels, (d) => d); 113 | 114 | this.labelOut.enter() 115 | .append('text') 116 | .attr('class', 'label-out'); 117 | 118 | this.labelOut 119 | .attr('x', scale(-0.5)) 120 | .style('text-anchor', 'middle') 121 | .text(prettifyBasis) 122 | .transition() 123 | .duration(toggleDuraton) 124 | .attr('y', (d, i) => scale(i + 0.5)) 125 | .attr('dy', '0.5em'); 126 | 127 | this.labelOut.exit() 128 | .remove(); 129 | 130 | // matrix elements 131 | 132 | if (matrixElements != null) { 133 | 134 | this.matrixElement = this.g 135 | .selectAll('.matrix-element') 136 | .data(matrixElements, (d) => `${d.from} ${d.to}`); 137 | 138 | this.matrixElement.enter() 139 | .append('rect') 140 | .attr('class', 'matrix-element') 141 | .on('mouseover', (d) => { 142 | const r = Math.sqrt(d.re * d.re + d.im * d.im); 143 | const phi = Math.atan2(d.im, d.re) / TAU; 144 | const sign = d.im >= 0 ? '+' : '-'; 145 | if (r > EPSILON) { 146 | this.tooltip.show( 147 | `${d.re.toFixed(3)} ${sign} ${Math.abs(d.im).toFixed(3)} i
148 | = ${r.toFixed(3)} exp(${phi.toFixed(3)} i \u03C4)` 149 | ); 150 | } 151 | }) 152 | .on('mouseout', () => this.tooltip.out()); 153 | 154 | } 155 | 156 | this.matrixElement 157 | .attr('width', squareSize - 1) 158 | .attr('height', squareSize - 1) 159 | .style('fill', complexToPureColor) 160 | .style('fill-opacity', complexToOpacity) 161 | .transition() 162 | .duration(toggleDuraton) 163 | .attr('y', (d) => scale(position[d.to]) + 0.5) 164 | .attr('x', (d) => scale(position[d.from]) + 0.5); 165 | 166 | this.matrixElement.exit() 167 | .remove(); 168 | 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /js/views/encyclopedia_item_view.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | 3 | import * as tile from '../tile'; 4 | import {tileSize} from '../config'; 5 | import {View} from './view'; 6 | import {TransitionHeatmap} from '../transition_heatmap'; 7 | 8 | export class EncyclopediaItemView extends View { 9 | get title() { 10 | return tile[this.game.currentEncyclopediaItem].desc.name; 11 | } 12 | get className() { 13 | return 'view--encyclopedia-item'; 14 | } 15 | initialize() { 16 | this.bindMenuEvents(); 17 | } 18 | resetContent() { 19 | if (!this.game.currentEncyclopediaItem) { 20 | return; 21 | } 22 | 23 | const tileData = tile[this.game.currentEncyclopediaItem]; 24 | 25 | const article = d3.select('.encyclopedia-item__container > article'); 26 | 27 | article 28 | .html(null); 29 | 30 | this.createBasicInfo(article, tileData); 31 | this.createTransitions(article, tileData); 32 | this.createHowItWorks(article, tileData); 33 | this.createUsage(article, tileData); 34 | } 35 | 36 | createBasicInfo(article, tileData) { 37 | article 38 | .append('h1') 39 | .attr('id', 'encyclopedia-item__basic-info') 40 | .text('Basic info'); 41 | article 42 | .append('svg') 43 | .attr('class', 'big-tile') 44 | .attr('viewBox', '0 0 100 100') 45 | .append('use') 46 | .attr('xlink:href', `#${tileData.svgName}`) 47 | .attr('transform', 'translate(50, 50)'); 48 | // draw method 49 | article 50 | .append('h4') 51 | .text(tileData.desc.name); 52 | article 53 | .append('div') 54 | .classed('content', true) 55 | .text(tileData.desc.summary); 56 | if (tileData.desc.flavour) { 57 | article 58 | .append('div') 59 | .classed('content', true) 60 | .append('i') 61 | .text(`"${tileData.desc.flavour}"`); 62 | } 63 | } 64 | 65 | createTransitions(article, tileData) { 66 | article 67 | .append('h1') 68 | .attr('id', 'encyclopedia-item__transitions') 69 | .text('Transitions'); 70 | 71 | article 72 | .append('p') 73 | .classed('encyclopedia-item__hint', true) 74 | .text('Click on heatmap to change its ordering (direction, polarization).'); 75 | 76 | const hmMatrixSize = 150; 77 | const hmTileSize = 50; 78 | 79 | const hm = article 80 | .append('div') 81 | .attr('class', 'content content--heatmap'); 82 | 83 | const hmSvg = hm.append('svg') 84 | .attr('viewBox', `0 0 ${hmMatrixSize + hmTileSize} ${hmMatrixSize}`) 85 | .attr('preserveAspectRatio', 'xMidYMid meet') 86 | .attr('class', 'content heatmap'); 87 | 88 | // TODO something for rotation... 89 | const tileObj = new tile.Tile(tileData); 90 | const transitionHeatmap = new TransitionHeatmap(hmSvg, hm, hmMatrixSize); 91 | transitionHeatmap.updateFromTensor(tileObj.transitionAmplitudes.map); 92 | 93 | hmSvg.append('text') 94 | .attr('class', 'hm-element-rotation-hint') 95 | .attr('x', hmMatrixSize + hmTileSize / 2) 96 | .attr('y', hmMatrixSize - hmTileSize) 97 | .style('font-size', '8px') 98 | .style('text-anchor', 'middle') 99 | .text('click to rotate'); 100 | 101 | tileObj.g = hmSvg.append('g') 102 | .attr('transform', `translate(${hmMatrixSize},${hmMatrixSize - hmTileSize})scale(${hmTileSize/tileSize})translate(${tileSize/2},${tileSize/2})`); 103 | tileObj.draw(); 104 | 105 | // rotation hitbox 106 | hmSvg.append('rect') 107 | .attr('class', 'helper-hitbox') 108 | .attr('x', hmMatrixSize) 109 | .attr('y', hmMatrixSize - 1.5 * hmTileSize) 110 | .attr('width', hmTileSize) 111 | .attr('height', 1.5 * hmTileSize) 112 | .attr('rx', 10) 113 | .attr('ry', 10) 114 | .on('click', () => { 115 | tileObj.rotate(); 116 | transitionHeatmap.updateFromTensor(tileObj.transitionAmplitudes.map); 117 | }); 118 | 119 | } 120 | 121 | createHowItWorks(article, tileData) { 122 | // TODO(pathes): content 123 | } 124 | 125 | createUsage(article, tileData) { 126 | // TODO(pathes): content 127 | } 128 | 129 | 130 | bindMenuEvents() { 131 | // Navigation between views 132 | d3.select('.bottom-bar__back-to-encyclopedia-selector-button').on('click', () => { 133 | this.game.setView('encyclopediaSelector'); 134 | }); 135 | // Navigation in encyclopedia entry 136 | const menuButtons = d3.selectAll('.encyclopedia-item__menu li button'); 137 | menuButtons.on('click', function () { 138 | const article = d3.select('.encyclopedia-item__container > article'); 139 | const headerIdSuffix = this.getAttribute('encyclopedia-nav'); 140 | const headerId = `encyclopedia-item__${headerIdSuffix}`; 141 | const header = window.document.getElementById(headerId); 142 | if (!header) { 143 | return; 144 | } 145 | article[0][0].scrollTop = header.offsetTop; 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /js/views/encyclopedia_selector_view.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | 3 | import {View} from './view'; 4 | import * as tile from '../tile'; 5 | 6 | export class EncyclopediaSelectorView extends View { 7 | get title() { 8 | return 'Encyclopedia'; 9 | } 10 | get className() { 11 | return 'view--encyclopedia-selector'; 12 | } 13 | initialize() { 14 | this.createSelectorEntries(); 15 | this.bindMenuEvents(); 16 | } 17 | createSelectorEntries() { 18 | const items = d3.select('.encyclopedia-selector > ul') 19 | .selectAll('li') 20 | .data(tile.nonVacuumTiles) 21 | .enter() 22 | .append('li') 23 | .append('button') 24 | .attr('class', 'unselectable') 25 | .on('click', (d) => { 26 | this.game.setEncyclopediaItem(d); 27 | this.game.setView('encyclopediaItem'); 28 | }); 29 | items 30 | .append('svg') 31 | .attr('viewBox', '0 0 100 100') 32 | .append('use') 33 | .attr('xlink:href', (d) => `#${tile[d].svgName}`) 34 | .attr('transform', 'translate(50, 50)'); 35 | items 36 | .append('h4') 37 | .text((d) => tile[d].desc.name); 38 | } 39 | bindMenuEvents() { 40 | d3.select('.view--encyclopedia-selector .bottom-bar__back-to-game-button').on('click', () => { 41 | this.game.setView('game'); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /js/views/game_view.js: -------------------------------------------------------------------------------- 1 | import {View} from './view'; 2 | 3 | export class GameView extends View { 4 | get title() { 5 | return this.game.gameBoard.title; 6 | } 7 | get className() { 8 | return 'view--game'; 9 | } 10 | initialize() { 11 | this.game.createGameBoard(); 12 | this.game.bindMenuEvents(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /js/views/level_selector_view.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import _ from 'lodash'; 3 | 4 | import {View} from './view'; 5 | import * as level from '../level'; 6 | 7 | export class LevelSelectorView extends View { 8 | get title() { 9 | return 'Quantum game'; 10 | } 11 | get className() { 12 | return 'view--level-selector'; 13 | } 14 | initialize() { 15 | const listOfElements = d3.select('.level-selector > ul') 16 | .selectAll('li') 17 | .data(level.levels) 18 | .enter() 19 | .append('li') 20 | .attr('class', 'level-item unselectable') 21 | .text((d) => `[${d.group}] ${d.i}. ${d.name} `) 22 | .on('click', (d) => { 23 | this.game.gameBoard.loadLevel(d.id); 24 | this.game.setView('game'); 25 | }); 26 | 27 | // as of now it is a version for developers 28 | // for users - graphical icons (of the new elements) or display:none; 29 | const elementsEncountered = {}; 30 | level.levels.forEach((d) => { 31 | d.newTiles = []; 32 | d.tiles.forEach((tile) => { 33 | if (!_.has(elementsEncountered, tile.name)) { 34 | elementsEncountered[tile.name] = true; 35 | d.newTiles.push(tile.name); 36 | } 37 | }); 38 | }); 39 | 40 | listOfElements.append('span') 41 | .style('font-size', '1.5vh') 42 | .text((d) => 43 | _(d.tiles) 44 | .groupBy('name') 45 | .keys() 46 | .filter((tile) => !_.includes(['Detector', 'Rock', 'Source'], tile)) 47 | .value() 48 | .join(' ') 49 | ); 50 | 51 | listOfElements.append('span') 52 | .style('font-size', '1.5vh') 53 | .text((d) => d.newTiles.length ? ` (NEW: ${d.newTiles.join(' ')})` : ''); 54 | 55 | this.bindMenuEvents() 56 | } 57 | bindMenuEvents() { 58 | d3.select('.view--level-selector .bottom-bar__back-to-game-button').on('click', () => { 59 | this.game.setView('game'); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /js/views/view.js: -------------------------------------------------------------------------------- 1 | export class View { 2 | constructor(game) { 3 | this.game = game; 4 | } 5 | initialize () {} 6 | } 7 | -------------------------------------------------------------------------------- /js/winning_status.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {Simulation} from './simulation'; 4 | import {EPSILON_DETECTION} from './const'; 5 | 6 | export class WinningStatus { 7 | 8 | constructor(tileMatrix) { 9 | this.tileMatrix = tileMatrix; 10 | } 11 | 12 | run() { 13 | const simulationC = new Simulation(this.tileMatrix); 14 | simulationC.initialize(); 15 | simulationC.propagateToEnd(false); 16 | 17 | this.absorptionProbabilities = _(simulationC.measurementHistory) 18 | .flatten() 19 | .groupBy((entry) => `${entry.i} ${entry.j}`) 20 | .mapValues((groupedEntry) => 21 | _.sumBy(groupedEntry, 'probability') 22 | ) 23 | .map((probability, location) => ({ 24 | probability: probability, 25 | i: parseInt(location.split(' ')[0]), 26 | j: parseInt(location.split(' ')[1]), 27 | })) 28 | .value(); 29 | 30 | this.probsAtDets = _(this.absorptionProbabilities) 31 | .filter((entry) => _.get(this.tileMatrix, `[${entry.i}][${entry.j}].isDetector`)) 32 | .map('probability') 33 | .value(); 34 | 35 | this.probsAtDetsByTime = _.map(simulationC.measurementHistory, (each) => 36 | _(each) 37 | .filter((entry) => _.get(this.tileMatrix, `[${entry.i}][${entry.j}].isDetector`)) 38 | .sumBy('probability') 39 | ); 40 | 41 | this.totalProbAtDets = _.sum(this.probsAtDets); 42 | this.noOfFedDets = this.probsAtDets 43 | .filter((probability) => probability > EPSILON_DETECTION) 44 | .length; 45 | this.probsAtMines = _(this.absorptionProbabilities) 46 | .filter((entry) => 47 | this.tileMatrix[entry.i] && this.tileMatrix[entry.i][entry.j] && this.tileMatrix[entry.i][entry.j].tileName === 'Mine' 48 | ) 49 | .sumBy('probability'); 50 | } 51 | 52 | compareToObjectives(requiredDetectionProbability, detectorsToFeed) { 53 | this.enoughProbability = this.totalProbAtDets > requiredDetectionProbability - EPSILON_DETECTION; 54 | this.enoughDetectors = this.noOfFedDets >= detectorsToFeed; 55 | this.noExplosion = this.probsAtMines < EPSILON_DETECTION; 56 | this.isWon = this.enoughProbability && this.enoughDetectors && this.noExplosion; 57 | const missingDets = detectorsToFeed - this.noOfFedDets; 58 | if (this.isWon) { 59 | this.message = 'You did it!'; 60 | } else if (!this.noExplosion) { 61 | this.message = `Nothing else matters when you have ${(100 * this.probsAtMines).toFixed(0)}% chance of setting off a mine!`; 62 | } else if (this.enoughProbability) { 63 | this.message = `${missingDets} detector${missingDets > 1 ? 's' : ''} feel${missingDets > 1 ? '' : 's'} sad and forgotten. Be fair! Give every detector a chance!`; 64 | } else if (this.totalProbAtDets > EPSILON_DETECTION) { 65 | this.message = `Only ${(100 * this.totalProbAtDets).toFixed(0)}% (out of ${(100 * requiredDetectionProbability).toFixed(0)}%) chance of detecting a photon at a detector. Try harder!`; 66 | } else { 67 | this.message = 'No chance to detect a photon at a detector.'; 68 | } 69 | 70 | return this.isWon; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /js/winning_status.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import {levels, Level} from './level'; 4 | import {WinningStatus} from './winning_status'; 5 | import * as tile from './tile'; 6 | 7 | //TODO some semi-working results (more fine-grained than isWon) 8 | 9 | //TODO probabilities for physical exercises 10 | 11 | //TODO modification of puzzles so they do not work (1 missing element?) 12 | 13 | const num2percent = (p) => `${(100 * p).toFixed(1)}%` 14 | 15 | describe('All game levels have solutions', () => { 16 | 17 | levels 18 | .filter((levelRecipe) => levelRecipe.group === 'Game') 19 | .forEach((levelRecipe) => { 20 | 21 | const level = new Level(levelRecipe, 'as_it_is'); 22 | 23 | // clearTileMatrix and fillTileMatrix from board.js 24 | const tileMatrix = _.range(level.width).map((i) => 25 | _.range(level.height).map((j) => 26 | new tile.Tile(tile.Vacuum, 0, false, i, j) 27 | ) 28 | ); 29 | 30 | _.each(level.tileRecipes, (tileRecipe) => { 31 | tileMatrix[tileRecipe.i][tileRecipe.j] = new tile.Tile( 32 | tile[tileRecipe.name], 33 | tileRecipe.rotation || 0, 34 | !!tileRecipe.frozen, 35 | tileRecipe.i, 36 | tileRecipe.j 37 | ); 38 | }); 39 | 40 | const winningStatus = new WinningStatus(tileMatrix); 41 | winningStatus.run(); 42 | winningStatus.compareToObjectives(level.requiredDetectionProbability, level.detectorsToFeed); 43 | 44 | it(`${level.i} ${level.name} (${level.detectorsToFeed} detectors at ${num2percent(level.requiredDetectionProbability)})`, () => { 45 | expect(winningStatus.isWon).toBe( 46 | true, 47 | `was: ${winningStatus.noOfFedDets} detectors at ${num2percent(winningStatus.totalProbAtDets)} with ${num2percent(winningStatus.probsAtMinesjasmine)} risk` 48 | ); 49 | }); 50 | 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu May 28 2015 13:00:00 GMT+0200 (CEST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jspm', 'jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | { 19 | pattern: 'data/**/*.json', included: false, 20 | }, 21 | ], 22 | 23 | jspm: { 24 | config: 'config.js', 25 | packages: 'jspm_packages/', 26 | useBundles: true, 27 | loadFiles: [ 28 | 'js/**/*.spec.js', 29 | ], 30 | serveFiles: [ 31 | 'js/**/*.js', 32 | ], 33 | }, 34 | 35 | 36 | // list of files to exclude 37 | exclude: [ 38 | ], 39 | 40 | 41 | // preprocess matching files before serving them to the browser 42 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 43 | preprocessors: { 44 | }, 45 | 46 | 47 | // test results reporter to use 48 | // possible values: 'dots', 'progress' 49 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 50 | reporters: ['spec'], 51 | // if progress is needed: $ karma start --reporters progress 52 | 53 | specReporter: { 54 | maxLogLines: 5, // limit number of lines logged per test 55 | suppressErrorSummary: true, // do not print error summary 56 | suppressFailed: false, // do not print information about failed tests 57 | suppressPassed: false, // do not print information about passed tests 58 | suppressSkipped: true, // do not print information about skipped tests 59 | }, 60 | 61 | 62 | plugins: ['karma-jspm', 'karma-jasmine', 'karma-chrome-launcher', 'karma-spec-reporter'], 63 | 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | 69 | // enable / disable colors in the output (reporters and logs) 70 | colors: true, 71 | 72 | 73 | // level of logging 74 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 75 | logLevel: config.LOG_INFO, 76 | 77 | 78 | // enable / disable watching file and executing tests whenever any file changes 79 | autoWatch: true, 80 | 81 | 82 | // start these browsers 83 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 84 | browsers: ['Chrome'], 85 | 86 | 87 | // Continuous Integration mode 88 | // if true, Karma captures browsers, runs the tests and exits 89 | singleRun: false, 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "jspm": { 3 | "dependencies": { 4 | "d3": "github:d3/d3@^3.5.5", 5 | "file-saver": "npm:file-saver@^1.3.1", 6 | "json": "github:systemjs/plugin-json@^0.1.0", 7 | "json-stringify-pretty-compact": "npm:json-stringify-pretty-compact@^1.0.1", 8 | "lodash": "npm:lodash@^4.13.1", 9 | "normalize.css": "github:necolas/normalize.css@^3.0.3", 10 | "soundjs": "github:CreateJS/SoundJS@^0.6.2" 11 | }, 12 | "devDependencies": { 13 | "babel": "npm:babel-core@^5.8.24", 14 | "babel-runtime": "npm:babel-runtime@^5.8.24", 15 | "clean-css": "npm:clean-css@^3.4.6", 16 | "core-js": "npm:core-js@^1.1.4" 17 | } 18 | }, 19 | "devDependencies": { 20 | "jasmine-core": "^2.3.4", 21 | "jspm": "^0.16.55", 22 | "karma": "^6.3.9", 23 | "karma-chrome-launcher": "^0.1.12", 24 | "karma-jasmine": "^0.2.2", 25 | "karma-jspm": "^2.0.1", 26 | "karma-spec-reporter": "0.0.23" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /screenshot_qg_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/screenshot_qg_dev.png -------------------------------------------------------------------------------- /sounds/absorber.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/absorber.mp3 -------------------------------------------------------------------------------- /sounds/blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/blip.mp3 -------------------------------------------------------------------------------- /sounds/detector.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/detector.mp3 -------------------------------------------------------------------------------- /sounds/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/error.mp3 -------------------------------------------------------------------------------- /sounds/mine.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/mine.mp3 -------------------------------------------------------------------------------- /sounds/rock.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stared/quantum-game/531710c7b2eff76f61ea2afc036398abf4741b34/sounds/rock.mp3 --------------------------------------------------------------------------------