├── icon.png ├── favicon.ico ├── .gitattributes ├── HKSuperRound-Bold.woff ├── .vscode └── tasks.json ├── LICENSE ├── tsconfig.json ├── style.css ├── index.html ├── main.js └── main.ts /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hedalu244/diffle/HEAD/icon.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hedalu244/diffle/HEAD/favicon.ico -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /HKSuperRound-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hedalu244/diffle/HEAD/HKSuperRound-Bold.woff -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "typescript", 8 | "tsconfig": "tsconfig.json", 9 | "option": "watch", 10 | "problemMatcher": [ 11 | "$tsc-watch" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hedalu244 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2021", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "none", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | "outFile": "./main.js", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'HK Super Round Bold'; 3 | font-style: normal; 4 | font-weight: normal; 5 | src: local('HK Super Round Bold'), url('HKSuperRound-Bold.woff') format('woff'); 6 | } 7 | 8 | html { 9 | font-size: min(2vw, 10px); 10 | color: #1a1a1b; 11 | font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif; 12 | } 13 | 14 | body { 15 | background-color: white; 16 | min-height: 100vh; 17 | margin: 0; 18 | font-size: 1.5rem; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | flex-flow: row nowrap; 23 | text-align: center; 24 | } 25 | 26 | button.icon { 27 | background: none; 28 | border: none; 29 | cursor: pointer; 30 | padding: 0 4px; 31 | } 32 | 33 | .wrapper { 34 | position: relative; 35 | width: 50rem; 36 | min-height: 100vh; 37 | margin: auto; 38 | display: flex; 39 | overflow: hidden; 40 | flex-flow: column nowrap; 41 | align-items: center; 42 | } 43 | 44 | header { 45 | background-color: white; 46 | height: 5rem; 47 | width: 50rem; 48 | display: flex; 49 | flex-flow: row nowrap; 50 | justify-content: center; 51 | align-items: center; 52 | border-bottom: 1px solid #e2e4e4; 53 | } 54 | 55 | .title { 56 | position:fixed; 57 | top: 0; 58 | z-index: 10; 59 | } 60 | 61 | h1 { 62 | font-family: 'HK Super Round Bold'; 63 | font-weight: 700; 64 | font-size: 3.6rem; 65 | padding-top: 0.4rem; 66 | letter-spacing: 0.1em; 67 | text-transform: uppercase; 68 | text-align: center; 69 | left: 0; 70 | right: 0; 71 | pointer-events: none; 72 | } 73 | h2 { 74 | font-family: 'HK Super Round Bold'; 75 | font-weight: 700; 76 | font-size: 2.5rem; 77 | text-transform: uppercase; 78 | padding-top: 0.4rem; 79 | letter-spacing: 0.1em; 80 | text-align: center; 81 | left: 0; 82 | right: 0; 83 | pointer-events: none; 84 | } 85 | 86 | #game { 87 | width: 100%; 88 | margin: 5rem 0 25rem 0; 89 | } 90 | 91 | #board { 92 | display: flex; 93 | flex-flow: column nowrap; 94 | align-items: center; 95 | flex: 1; 96 | } 97 | 98 | .guess { 99 | text-align: center; 100 | white-space: nowrap; 101 | margin: 1rem 0; 102 | } 103 | 104 | .empty::before { 105 | content: ""; 106 | display: block; 107 | width: 4.2rem; 108 | height: 4.2rem; 109 | border-radius: 50%; 110 | border: 1px solid #cacaca; 111 | margin: 0 .2rem; 112 | 113 | text-align: center; 114 | vertical-align: middle; 115 | text-transform: uppercase; 116 | font-weight: bold; 117 | } 118 | 119 | .letter { 120 | font-family: 'HK Super Round Bold'; 121 | display: inline-block; 122 | position: relative; 123 | font-size: 2.3rem; 124 | line-height: 4.9rem; 125 | width: 4.2rem; 126 | height: 4.2rem; 127 | border-radius: 50%; 128 | border: 1px solid #cacaca; 129 | margin: 0 .2rem; 130 | 131 | text-align: center; 132 | vertical-align: middle; 133 | text-transform: uppercase; 134 | font-weight: bold; 135 | } 136 | 137 | .letter.absent { 138 | background-color: #959b9d; 139 | color: white; 140 | border: none; 141 | } 142 | 143 | .letter.present { 144 | background-color: #e8b838; 145 | color: white; 146 | border: none; 147 | } 148 | 149 | .letter.head { 150 | background-color: #4fb061; 151 | color: white; 152 | border: none; 153 | } 154 | 155 | .letter.tail { 156 | background-color: #4fb061; 157 | color: white; 158 | border: none; 159 | } 160 | 161 | .letter.tail::before { 162 | content: ""; 163 | display: block; 164 | position: absolute; 165 | background-color: #4fb061; 166 | z-index: -10; 167 | top: 0; 168 | bottom: 0; 169 | left: -.2rem; 170 | right: -.2rem; 171 | transform: translate(-50%, 0); 172 | } 173 | 174 | .letter.start::after { 175 | content: ""; 176 | display: block; 177 | position: absolute; 178 | background-color: #4fb061; 179 | z-index: -10; 180 | top: 0; 181 | bottom: 0; 182 | left: -.2rem; 183 | right: 50%; 184 | } 185 | 186 | .letter.end::after { 187 | content: ""; 188 | display: block; 189 | position: absolute; 190 | background-color: #4fb061; 191 | z-index: -10; 192 | top: 0; 193 | bottom: 0; 194 | right: -.2rem; 195 | left: 50%; 196 | } 197 | 198 | #keyboard { 199 | position: fixed; 200 | bottom: 0; 201 | width: 50rem; 202 | display: flex; 203 | flex-flow: column nowrap; 204 | user-select: none; 205 | touch-action: manipulation; 206 | background-color: white; 207 | } 208 | 209 | #keyboard>div { 210 | display: flex; 211 | flex-flow: row nowrap; 212 | justify-content: center; 213 | margin: 0.4rem 0; 214 | } 215 | 216 | #keyboard button { 217 | font-family: 'HK Super Round Bold'; 218 | font-weight: bold; 219 | font-size: 1.5rem; 220 | height: 5.8rem; 221 | line-height: 6rem; 222 | flex: 1; 223 | outline: none; 224 | border: none; 225 | cursor: pointer; 226 | user-select: none; 227 | background-color: #e2e4e4; 228 | color: #1a1a1b; 229 | border-radius: 0.4rem; 230 | margin: 0 0.3rem; 231 | } 232 | 233 | 234 | #keyboard button#keyboard_backspace { 235 | display: flex; 236 | justify-content: center; 237 | align-items: center; 238 | } 239 | 240 | #keyboard button.absent { 241 | background-color: #959b9d; 242 | color: white; 243 | } 244 | 245 | #keyboard button.present { 246 | background-color: #e8b838; 247 | color: white; 248 | } 249 | 250 | #keyboard button.correct { 251 | background-color: #4fb061; 252 | color: white; 253 | } 254 | 255 | #keyboard .spacer { 256 | flex: 0.5; 257 | } 258 | 259 | #result { 260 | width: 100%; 261 | margin: 5rem 0; 262 | } 263 | 264 | .result_count { 265 | font-family: 'HK Super Round Bold'; 266 | display: flex; 267 | flex-flow: row; 268 | justify-content: center; 269 | align-items: baseline; 270 | } 271 | 272 | .result_count_label { 273 | line-height: 2rem; 274 | font-size: 1.8rem; 275 | text-align: Left; 276 | padding-left: 2rem; 277 | display: inline-block; 278 | } 279 | 280 | #letters_used, #words_used { 281 | display: block; 282 | font-size: 8rem; 283 | line-height: 8rem; 284 | } 285 | 286 | .slash { 287 | height: 9rem; 288 | width: 0; 289 | border-left: 1px solid #1a1a1b; 290 | transform: rotate(20deg); 291 | } 292 | 293 | .timer { 294 | font-family: 'HK Super Round Bold'; 295 | width: 100%; 296 | } 297 | 298 | .timer>h2 { 299 | text-align: center; 300 | margin-bottom: 10px; 301 | } 302 | 303 | #timer { 304 | font-size: 36px; 305 | font-weight: 400; 306 | display: flex; 307 | align-items: center; 308 | justify-content: center; 309 | text-align: center; 310 | letter-spacing: 0.05em; 311 | } 312 | 313 | .share { 314 | display: flex; 315 | justify-content: center; 316 | align-items: center; 317 | } 318 | 319 | #share_button, #share_image_button { 320 | background-color: #4fb061; 321 | color: white; 322 | font-family: inherit; 323 | font-weight: bold; 324 | border-radius: 4px; 325 | cursor: pointer; 326 | border: none; 327 | user-select: none; 328 | display: flex; 329 | justify-content: center; 330 | align-items: center; 331 | text-transform: uppercase; 332 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0.3); 333 | width: 40%; 334 | font-size: 20px; 335 | margin: 0 1rem; 336 | height: 52px; 337 | } 338 | #share_image_button { 339 | width: 52px; 340 | } 341 | 342 | #help, #stats{ 343 | z-index: 20; 344 | position: fixed; 345 | top: 100vh; 346 | width: 50rem; 347 | height: 100vh; 348 | overflow: hidden; 349 | background-color: white; 350 | opacity: 0; 351 | transition: 0.3s ease-in-out; 352 | } 353 | 354 | input[type=radio]:checked+#help, input[type=radio]:checked+#stats { 355 | opacity: 1; 356 | top: 0; 357 | } 358 | 359 | .example { 360 | margin: 1.5rem; 361 | } 362 | 363 | .example .guess { 364 | margin: 0; 365 | } 366 | 367 | .example p { 368 | margin: .5rem; 369 | } 370 | 371 | 372 | .stats_container { 373 | font-family: 'HK Super Round Bold'; 374 | display: flex; 375 | flex-flow: row nowrap; 376 | justify-content: space-around; 377 | margin: 4rem 0; 378 | } 379 | 380 | .stats_number { 381 | font-size: 4rem; 382 | line-height: 4rem; 383 | text-align: center; 384 | } 385 | 386 | .stats_label { 387 | line-height: 2rem; 388 | font-size: 1.8rem; 389 | text-align: center; 390 | } 391 | 392 | #alert { 393 | font-family: 'HK Super Round Bold'; 394 | font-size: 1.5rem; 395 | letter-spacing: 0.1rem; 396 | 397 | position: fixed; 398 | left: auto; 399 | right: auto; 400 | color: white; 401 | padding: 2rem; 402 | background-color: #1a1a1b; 403 | box-shadow: #0005 0px 5px 20px; 404 | 405 | opacity: 0; 406 | top: -30vh; 407 | transition: 0.3s ease-in-out; 408 | } 409 | 410 | #alert.visible { 411 | opacity: 1; 412 | top: 15vh; 413 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Diffle 9 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

DIFFLE

19 | 26 | 32 |
33 |
34 |
35 |
36 |
37 | 68 |
69 |
70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 111 |
112 |
113 | 114 | 115 | 116 |
117 |
118 |

How To Play

119 | 126 |
127 | 128 |

This game is inspired by Wordle.

129 | 130 |

Guess the word using as few letters as possible.

131 | 132 |

Each guess must be a valid word. Hit the enter button to submit.

133 | 134 |

After each guess, the color of the tiles will change to show how close your guess was to the word. 135 |

136 | 137 | 138 |

Examples

139 |
140 |
141 |
s
142 |
h
143 |
i
144 |
r
145 |
t
146 |
147 |

Gray letters are not in the word.

148 |
149 |
150 |
151 |
p
152 |
e
153 |
d
154 |
a
155 |
l
156 |
157 |

The letters E, A, and L are in the word in this order.

158 |

The letter P is in the word but not before the E.

159 |
160 |
161 |
162 |
i
163 |
m
164 |
p
165 |
l
166 |
y
167 |
168 |

The letter MPL is in the word in a row.

169 |
170 |
171 |
172 |
e
173 |
d
174 |
i
175 |
b
176 |
l
177 |
e
178 |
179 |

The word starts with E and ends with LE.

180 |
181 |
182 |
183 |
e
184 |
x
185 |
a
186 |
m
187 |
p
188 |
l
189 |
e
190 |
191 |

This is the answer word.

192 |
193 |
194 | 195 | 196 |
197 |
198 |

Statistics

199 | 206 |
207 | 208 |
209 |
210 |
211 |
Played
212 |
213 |
214 |
215 |
Won
216 |
217 |
218 |
219 |
Average
Words
220 |
221 |
222 |
223 |
Average
Letters
224 |
225 |
226 | 230 |
231 |
232 |
not in word list
233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function diffle(answer, guess) { 3 | const table = Array.from({ length: answer.length + 1 }, () => Array.from({ length: guess.length + 1 }, () => ({ cost: 0, paths: [], }))); 4 | table[0][0] = { cost: 0, paths: [[]] }; 5 | for (let a = 1; a < answer.length + 1; a++) 6 | table[a][0] = { cost: a, paths: table[a - 1][0].paths.map(x => [...x, "+"]) }; 7 | for (let b = 1; b < guess.length + 1; b++) 8 | table[0][b] = { cost: b, paths: table[0][b - 1].paths.map(x => [...x, "-"]) }; 9 | for (let a = 1; a < answer.length + 1; a++) { 10 | for (let b = 1; b < guess.length + 1; b++) { 11 | const accept = table[a - 1][b - 1].cost + (answer[a - 1] == guess[b - 1] ? 0 : Infinity); 12 | const insert = table[a - 1][b].cost + 1; 13 | const remove = table[a][b - 1].cost + 1; 14 | const cost = Math.min(insert, remove, accept); 15 | const paths = []; 16 | if (cost == accept) 17 | paths.push(...table[a - 1][b - 1].paths.map(x => [...x, ">"])); 18 | if (cost == insert) 19 | paths.push(...table[a - 1][b].paths.map(x => [...x, "+"])); 20 | if (cost == remove) 21 | paths.push(...table[a][b - 1].paths.map(x => [...x, "-"])); 22 | table[a][b] = { cost, paths }; 23 | } 24 | } 25 | let best_score = -Infinity; 26 | let best_results = []; 27 | table[answer.length][guess.length].paths.forEach(path => { 28 | const start = path[0] == ">"; 29 | const end = path[path.length - 1] == ">"; 30 | const pattern = Array.from({ length: guess.length }, x => 0); 31 | const unused_letter = Array.from(answer); // answerの中でまだ使ってない文字 32 | let accept_count = 0; 33 | let streak_length = 0; 34 | let score = 0; 35 | if (start) 36 | score += 1; 37 | if (end) 38 | score += 1; 39 | let a = 0, b = 0; 40 | for (let i = 0; i < path.length; i++) { 41 | switch (path[i]) { 42 | case ">": 43 | accept_count++; 44 | streak_length++; 45 | pattern[b] = streak_length == 1 ? 2 : 3; 46 | unused_letter.splice(unused_letter.indexOf(guess[b]), 1); 47 | score += 3 * streak_length; 48 | a++; 49 | b++; 50 | break; 51 | case "+": 52 | streak_length = 0; 53 | a++; 54 | break; 55 | case "-": 56 | streak_length = 0; 57 | b++; 58 | break; 59 | } 60 | } 61 | // 黄色を生成 62 | for (let i = 0; i < guess.length; i++) { 63 | if (pattern[i] == 0 && unused_letter.includes(guess[i])) { 64 | pattern[i] = 1; 65 | unused_letter.splice(unused_letter.indexOf(guess[i]), 1); 66 | } 67 | } 68 | // 緑が一文字のとき黄色に変換 69 | if (accept_count == 1 && !start && !end) { 70 | pattern[pattern.indexOf(2)] = 1; 71 | } 72 | if (best_score == score) { 73 | best_results.push({ pattern, start, end }); 74 | } 75 | else if (best_score < score) { 76 | best_score = score; 77 | best_results = [{ pattern, start, end }]; 78 | } 79 | }); 80 | best_results.sort((a, b) => a.pattern.join() < b.pattern.join() ? 1 : -1); 81 | return best_results[0]; 82 | } 83 | function assure(a, b) { 84 | if (a instanceof b) 85 | return a; 86 | throw new TypeError(`${a} is not ${b.name}.`); 87 | } 88 | const $inputRow = assure(document.getElementById("input_row"), HTMLDivElement); 89 | const $board = assure(document.getElementById("board"), HTMLDivElement); 90 | let play; 91 | let stats; 92 | function dailySeed() { 93 | const now = new Date(); 94 | return now.getDate() + now.getMonth() * 32 + now.getFullYear() * 400; 95 | } 96 | function getAnswer(seed) { 97 | if (seed == 808836) 98 | return "differ"; 99 | let x = 123456789; 100 | let y = 362436069; 101 | let z = 521288629; 102 | let w = seed; 103 | for (let i = 0; i < 1024; i++) { 104 | let t = x ^ (x << 11); 105 | x = y; 106 | y = z; 107 | z = w; 108 | w = (w ^ (w >>> 19)) ^ (t ^ (t >>> 8)); 109 | } 110 | return answers[Math.abs(w) % answers.length]; 111 | } 112 | function save() { 113 | localStorage.setItem("diffle_play", JSON.stringify(play)); 114 | localStorage.setItem("diffle_stats", JSON.stringify(stats)); 115 | } 116 | function load() { 117 | const today = getTodayString(); 118 | const statsString = localStorage.getItem("diffle_stats"); 119 | stats = statsString ? JSON.parse(statsString) : {}; 120 | if (stats.played == undefined) 121 | stats.played = 0; 122 | if (stats.won == undefined) 123 | stats.won = 0; 124 | if (stats.total_guess_count == undefined) 125 | stats.total_guess_count = 0; 126 | if (stats.total_letter_count == undefined) 127 | stats.total_letter_count = 0; 128 | if (stats.played === 0) { 129 | assure(document.getElementById("open_help"), HTMLInputElement).checked = true; 130 | } 131 | const playString = localStorage.getItem("diffle_play"); 132 | const _play = playString ? JSON.parse(playString) : null; 133 | if (_play && _play.date == today) { 134 | play = _play; 135 | if (play.answer == undefined) 136 | play.answer = getAnswer(dailySeed()); 137 | play.history.forEach(x => insertGuess(x)); 138 | Array.from(play.guess).forEach(x => insertLetter(x)); 139 | if (play.history[play.history.length - 1] == play.answer) 140 | showReault(); 141 | } 142 | else { 143 | play = { 144 | date: today, 145 | guess: "", 146 | history: [], 147 | answer: getAnswer(dailySeed()), 148 | letter_count: 0, 149 | }; 150 | save(); 151 | } 152 | showStats(); 153 | } 154 | function insertLetter(letter) { 155 | const letter_element = document.createElement("div"); 156 | letter_element.className = "letter"; 157 | letter_element.textContent = letter; 158 | $inputRow.appendChild(letter_element); 159 | $inputRow.classList.remove("empty"); 160 | } 161 | function insertGuess(guess) { 162 | const row = document.createElement("div"); 163 | row.className = "guess"; 164 | const result = diffle(play.answer, guess); 165 | Array.from(guess).forEach((letter, i) => { 166 | const letter_element = document.createElement("div"); 167 | letter_element.className = "letter"; 168 | letter_element.textContent = letter; 169 | letter_element.classList.add(["absent", "present", "head", "tail"][result.pattern[i]]); 170 | const keyboard_button = assure(document.getElementById("keyboard_" + letter), HTMLButtonElement); 171 | if (result.pattern[i] == 0 172 | && keyboard_button.className !== "present" 173 | && keyboard_button.className !== "correct") 174 | keyboard_button.className = "absent"; 175 | if (result.pattern[i] == 1 176 | && keyboard_button.className !== "correct") 177 | keyboard_button.className = "present"; 178 | if (result.pattern[i] == 2 || result.pattern[i] == 3) 179 | keyboard_button.className = "correct"; 180 | if (i == 0 && result.start) 181 | letter_element.classList.add("start"); 182 | if (i == guess.length - 1 && result.end) 183 | letter_element.classList.add("end"); 184 | row.appendChild(letter_element); 185 | }); 186 | $board.insertBefore(row, $inputRow); 187 | $inputRow.innerHTML = ""; 188 | $inputRow.classList.add("empty"); 189 | } 190 | function inputLetter(letter) { 191 | if (play.history[play.history.length - 1] == play.answer) 192 | return; 193 | if (!/^[a-z]$/.test(letter)) 194 | throw new Error("invalid input"); 195 | if (10 <= play.guess.length) 196 | return; 197 | insertLetter(letter); 198 | play.guess += letter; 199 | save(); 200 | } 201 | function inputBackspace() { 202 | if (play.history[play.history.length - 1] == play.answer) 203 | return; 204 | if ($inputRow.lastElementChild) 205 | $inputRow.removeChild($inputRow.lastElementChild); 206 | if (play.guess !== "") 207 | play.guess = play.guess.substring(0, play.guess.length - 1); 208 | if (play.guess == "") 209 | $inputRow.classList.add("empty"); 210 | save(); 211 | } 212 | function enter() { 213 | if (play.history[play.history.length - 1] == play.answer) 214 | return; 215 | if (!allowed.includes(play.guess)) { 216 | myAlert("not in word list"); 217 | return; 218 | } 219 | if (play.history.length == 0) { 220 | stats.played++; 221 | showStats(); 222 | } 223 | insertGuess(play.guess); 224 | play.letter_count += play.guess.length; 225 | play.history.push(play.guess); 226 | if (play.guess == play.answer) { 227 | if (play.history.length <= 1) 228 | myAlert("miracle!"); 229 | else if (play.history.length <= 3) 230 | myAlert("genius!"); 231 | else if (play.history.length <= 6) 232 | myAlert("excellent!"); 233 | else if (play.history.length <= 10) 234 | myAlert("great!"); 235 | else 236 | myAlert("good!"); 237 | stats.won++; 238 | stats.total_guess_count += play.history.length; 239 | stats.total_letter_count += play.letter_count; 240 | showReault(); 241 | showStats(); 242 | } 243 | play.guess = ""; 244 | save(); 245 | } 246 | function showReault() { 247 | $inputRow.style.display = "none"; 248 | assure(document.getElementById("result"), HTMLDivElement).style.display = ""; 249 | assure(document.getElementById("timer_container"), HTMLDivElement).style.display = ""; 250 | assure(document.getElementById("letters_used"), HTMLDivElement).textContent = "" + play.letter_count; 251 | assure(document.getElementById("words_used"), HTMLDivElement).textContent = "" + play.history.length; 252 | assure(document.getElementById("words_used_label"), HTMLSpanElement).innerHTML = play.history.length <= 1 ? "Word
Used" : "Words
Used"; 253 | } 254 | function showStats() { 255 | assure(document.getElementById("stats_played"), HTMLDivElement).textContent = "" + stats.played; 256 | assure(document.getElementById("stats_won"), HTMLDivElement).textContent = "" + stats.won; 257 | assure(document.getElementById("stats_average_words"), HTMLDivElement).textContent = stats.won == 0 ? "0.0" : (stats.total_guess_count / stats.won).toFixed(1); 258 | assure(document.getElementById("stats_average_letters"), HTMLDivElement).textContent = stats.won == 0 ? "0.0" : (stats.total_letter_count / stats.won).toFixed(1); 259 | } 260 | function myAlert(message) { 261 | const alert = assure(document.getElementById("alert"), HTMLDivElement); 262 | alert.textContent = message; 263 | alert.classList.add("visible"); 264 | setTimeout(() => alert.classList.remove("visible"), 1500); 265 | } 266 | function share() { 267 | const title = "Diffle " + play.date + "\n"; 268 | const result = play.history.length + (play.history.length <= 1 ? " word / " : " words / ") + play.letter_count + " letters\n\n"; 269 | const pattern = play.history.map((x, i) => diffle(play.answer, x).pattern.map(y => i == play.history.length - 1 ? "\ud83d\udfe9" : y == 0 ? "\u26AA" : y == 1 ? "\ud83d\udfe1" : "\ud83d\udfe2").join("")).join("\n"); 270 | const url = location.href; 271 | navigator.clipboard.writeText(title + result + pattern + "\n\n" + url).then(function () { 272 | myAlert('Copyed results to clipboard'); 273 | }).catch(function (error) { 274 | myAlert(error.message); 275 | }); 276 | } 277 | function shareImage() { 278 | const width = 500; 279 | const circle_radius = 21; 280 | const dot_radius = 4; 281 | const margin_x = 2; 282 | const margin_y = 10; 283 | const header_height = 70; 284 | const canvas = document.createElement("canvas"); 285 | canvas.width = width; 286 | canvas.height = play.history.length * (circle_radius + margin_y) * 2 + header_height; 287 | const context = assure(canvas.getContext("2d"), CanvasRenderingContext2D); 288 | context.fillStyle = "#ffffff"; 289 | context.fillRect(0, 0, canvas.width, canvas.height); 290 | context.fillStyle = "#1a1a1b"; 291 | context.font = "40px 'HK Super Round Bold'"; 292 | context.textAlign = "center"; 293 | context.fillText("Diffle".split("").join(String.fromCharCode(8202)), width / 2, 40); 294 | context.font = "20px 'HK Super Round Bold'"; 295 | context.textAlign = "center"; 296 | context.fillText(play.date, width / 2, 65); 297 | play.history.forEach((guess, i) => { 298 | const center_y = (2 * i + 1) * (circle_radius + margin_y) + header_height; 299 | const result = diffle(play.answer, guess); 300 | if (guess == play.answer) { 301 | context.fillStyle = "#4fb061"; 302 | context.fillRect(0, center_y - circle_radius, width, circle_radius * 2); 303 | return; 304 | } 305 | if (result.start) { 306 | context.fillStyle = "#4fb061"; 307 | context.fillRect(width / 2 - guess.length * (circle_radius + margin_x), center_y - circle_radius, circle_radius + margin_x, circle_radius * 2); 308 | } 309 | if (result.end) { 310 | context.fillStyle = "#4fb061"; 311 | context.fillRect(width / 2 + (guess.length - 1) * (circle_radius + margin_x), center_y - circle_radius, circle_radius + margin_x, circle_radius * 2); 312 | } 313 | result.pattern.forEach((color, j) => { 314 | const center_x = width / 2 + (1 + 2 * j - guess.length) * (circle_radius + margin_x); 315 | context.beginPath(); 316 | context.arc(center_x, center_y, circle_radius, 0, 360 * Math.PI / 180, false); 317 | context.fillStyle = ["#959b9d", "#e8b838", "#4fb061", "#4fb061"][color]; 318 | context.fill(); 319 | if (color == 3) { 320 | context.fillStyle = "#4fb061"; 321 | context.fillRect(center_x - (circle_radius + margin_x) * 2, center_y - circle_radius, (circle_radius + margin_x) * 2, circle_radius * 2); 322 | context.fill(); 323 | } 324 | }); 325 | }); 326 | canvas.toBlob(blob => { 327 | try { 328 | if (blob == null) { 329 | throw new Error("something went wrong"); 330 | } 331 | var fileURL = URL.createObjectURL(blob); 332 | window.open(fileURL); 333 | } 334 | catch (err) { 335 | console.log(err); 336 | } 337 | }, "image/png"); 338 | } 339 | document.addEventListener("keydown", (ev) => { 340 | if (ev.key == "Backspace") 341 | inputBackspace(); 342 | if (ev.key == "Enter") 343 | enter(); 344 | if (/^[A-Za-z]$/.test(ev.key)) 345 | inputLetter(ev.key.toLowerCase()); 346 | }); 347 | Array.from("qwertyuiopasdfghjklzxcvbnm").forEach(letter => { 348 | const keyboard_button = assure(document.getElementById("keyboard_" + letter), HTMLButtonElement); 349 | keyboard_button.addEventListener("click", () => inputLetter(letter)); 350 | }); 351 | function getTodayString() { 352 | const now = new Date(); 353 | return `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; 354 | } 355 | function updateTimer() { 356 | const today = getTodayString(); 357 | if (play.date !== today) { 358 | assure(document.getElementById("timer"), HTMLDivElement).textContent = "00:00:00"; 359 | return; 360 | } 361 | const now = new Date(); 362 | const rest = 86400 - (3600 * now.getHours() + 60 * now.getMinutes() + now.getSeconds()); 363 | const rest_hours = Math.floor(rest / 3600); 364 | const rest_minutes = Math.floor((rest - 3600 * rest_hours) / 60); 365 | const rest_seconds = rest - 3600 * rest_hours - 60 * rest_minutes; 366 | const rest_format = `${("" + rest_hours).padStart(2, "0")}:${("" + rest_minutes).padStart(2, "0")}:${("" + rest_seconds).padStart(2, "0")}`; 367 | assure(document.getElementById("timer"), HTMLDivElement).textContent = rest_format; 368 | } 369 | assure(document.getElementById("keyboard_enter"), HTMLButtonElement).addEventListener("click", enter); 370 | assure(document.getElementById("keyboard_backspace"), HTMLButtonElement).addEventListener("click", inputBackspace); 371 | assure(document.getElementById("share_button"), HTMLButtonElement).addEventListener("click", share); 372 | assure(document.getElementById("share_image_button"), HTMLButtonElement).addEventListener("click", shareImage); 373 | load(); 374 | setInterval(updateTimer, 1000); 375 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | interface DiffleResult { 2 | pattern: (0 | 1 | 2 | 3)[], 3 | start: boolean, 4 | end: boolean, 5 | } 6 | 7 | function diffle(answer: string, guess: string): DiffleResult { 8 | const table = Array.from({ length: answer.length + 1 }, () => Array.from({ length: guess.length + 1 }, () => ( 9 | { cost: 0, paths: [] as ("+" | "-" | ">")[][], } 10 | ))); 11 | 12 | table[0][0] = { cost: 0, paths: [[]] }; 13 | for (let a = 1; a < answer.length + 1; a++) 14 | table[a][0] = { cost: a, paths: table[a - 1][0].paths.map(x => [...x, "+"]) }; 15 | for (let b = 1; b < guess.length + 1; b++) 16 | table[0][b] = { cost: b, paths: table[0][b - 1].paths.map(x => [...x, "-"]) }; 17 | 18 | for (let a = 1; a < answer.length + 1; a++) { 19 | for (let b = 1; b < guess.length + 1; b++) { 20 | const accept = table[a - 1][b - 1].cost + (answer[a - 1] == guess[b - 1] ? 0 : Infinity); 21 | const insert = table[a - 1][b].cost + 1; 22 | const remove = table[a][b - 1].cost + 1; 23 | 24 | const cost = Math.min(insert, remove, accept); 25 | const paths = [] as ("+" | "-" | ">")[][]; 26 | 27 | if (cost == accept) paths.push(...table[a - 1][b - 1].paths.map(x => [...x, ">" as const])); 28 | if (cost == insert) paths.push(...table[a - 1][b].paths.map(x => [...x, "+" as const])); 29 | if (cost == remove) paths.push(...table[a][b - 1].paths.map(x => [...x, "-" as const])); 30 | 31 | table[a][b] = { cost, paths }; 32 | } 33 | } 34 | 35 | let best_score = -Infinity; 36 | let best_results: DiffleResult[] = []; 37 | 38 | table[answer.length][guess.length].paths.forEach(path => { 39 | const start = path[0] == ">"; 40 | const end = path[path.length - 1] == ">"; 41 | const pattern: (0 | 1 | 2 | 3)[] = Array.from({ length: guess.length }, x => 0); 42 | const unused_letter: string[] = Array.from(answer); // answerの中でまだ使ってない文字 43 | 44 | let accept_count = 0; 45 | let streak_length = 0; 46 | let score = 0; 47 | if (start) score += 1; 48 | if (end) score += 1; 49 | 50 | 51 | let a = 0, b = 0; 52 | for (let i = 0; i < path.length; i++) { 53 | switch (path[i]) { 54 | case ">": 55 | accept_count++; 56 | streak_length++; 57 | pattern[b] = streak_length == 1 ? 2 : 3; 58 | unused_letter.splice(unused_letter.indexOf(guess[b]), 1); 59 | score += 3 * streak_length; 60 | a++; 61 | b++; 62 | break; 63 | case "+": 64 | streak_length = 0; 65 | a++; 66 | break; 67 | case "-": 68 | streak_length = 0; 69 | b++; 70 | break; 71 | } 72 | } 73 | 74 | // 黄色を生成 75 | for (let i = 0; i < guess.length; i++) { 76 | if (pattern[i] == 0 && unused_letter.includes(guess[i])) { 77 | pattern[i] = 1; 78 | unused_letter.splice(unused_letter.indexOf(guess[i]), 1); 79 | } 80 | } 81 | // 緑が一文字のとき黄色に変換 82 | if (accept_count == 1 && !start && !end) { 83 | pattern[pattern.indexOf(2)] = 1; 84 | } 85 | 86 | if (best_score == score) { 87 | best_results.push({ pattern, start, end }); 88 | } else if (best_score < score) { 89 | best_score = score; 90 | best_results = [{ pattern, start, end }]; 91 | } 92 | }); 93 | 94 | best_results.sort((a, b) => a.pattern.join() < b.pattern.join() ? 1 : -1); 95 | return best_results[0]; 96 | } 97 | 98 | function assure any>(a: any, b: T): InstanceType { 99 | if (a instanceof b) return a; 100 | throw new TypeError(`${a} is not ${b.name}.`); 101 | } 102 | 103 | const $inputRow = assure(document.getElementById("input_row"), HTMLDivElement); 104 | const $board = assure(document.getElementById("board"), HTMLDivElement); 105 | 106 | interface PlayData { 107 | date: string; 108 | guess: string; 109 | letter_count: number; 110 | answer: string; 111 | history: string[]; 112 | } 113 | interface StatsData { 114 | played: number; 115 | won: number; 116 | total_guess_count: number; 117 | total_letter_count: number; 118 | } 119 | let play: PlayData; 120 | let stats: StatsData; 121 | 122 | function dailySeed() { 123 | const now = new Date(); 124 | return now.getDate() + now.getMonth() * 32 + now.getFullYear() * 400; 125 | } 126 | 127 | function getAnswer(seed: number): string { 128 | if (seed == 808836) return "differ"; 129 | 130 | let x = 123456789; 131 | let y = 362436069; 132 | let z = 521288629; 133 | let w = seed; 134 | for (let i = 0; i < 1024; i++) { 135 | let t = x ^ (x << 11); 136 | x = y; 137 | y = z; 138 | z = w; 139 | w = (w ^ (w >>> 19)) ^ (t ^ (t >>> 8)); 140 | } 141 | 142 | return answers[Math.abs(w) % answers.length]; 143 | } 144 | 145 | function save() { 146 | localStorage.setItem("diffle_play", JSON.stringify(play)); 147 | localStorage.setItem("diffle_stats", JSON.stringify(stats)); 148 | } 149 | 150 | function load() { 151 | const today = getTodayString(); 152 | 153 | const statsString = localStorage.getItem("diffle_stats"); 154 | stats = statsString ? JSON.parse(statsString) : {} as StatsData; 155 | 156 | if (stats.played == undefined) stats.played = 0; 157 | if (stats.won == undefined) stats.won = 0; 158 | if (stats.total_guess_count == undefined) stats.total_guess_count = 0; 159 | if (stats.total_letter_count == undefined) stats.total_letter_count = 0; 160 | 161 | if (stats.played === 0) { 162 | assure(document.getElementById("open_help"), HTMLInputElement).checked = true; 163 | } 164 | 165 | const playString = localStorage.getItem("diffle_play"); 166 | const _play = playString ? JSON.parse(playString) as PlayData : null; 167 | 168 | if (_play && _play.date == today) { 169 | play = _play; 170 | if (play.answer == undefined) play.answer = getAnswer(dailySeed()); 171 | play.history.forEach(x => insertGuess(x)); 172 | 173 | Array.from(play.guess).forEach(x => insertLetter(x)); 174 | if (play.history[play.history.length - 1] == play.answer) showReault(); 175 | } 176 | else { 177 | play = { 178 | date: today, 179 | guess: "", 180 | history: [], 181 | answer: getAnswer(dailySeed()), 182 | letter_count: 0, 183 | }; 184 | save(); 185 | } 186 | 187 | showStats(); 188 | } 189 | 190 | function insertLetter(letter: string) { 191 | const letter_element = document.createElement("div"); 192 | letter_element.className = "letter"; 193 | letter_element.textContent = letter; 194 | $inputRow.appendChild(letter_element); 195 | $inputRow.classList.remove("empty"); 196 | } 197 | 198 | function insertGuess(guess: string) { 199 | const row = document.createElement("div"); 200 | row.className = "guess"; 201 | 202 | const result = diffle(play.answer, guess); 203 | 204 | Array.from(guess).forEach((letter, i) => { 205 | const letter_element = document.createElement("div"); 206 | letter_element.className = "letter"; 207 | letter_element.textContent = letter; 208 | letter_element.classList.add(["absent", "present", "head", "tail"][result.pattern[i]]); 209 | 210 | const keyboard_button = assure(document.getElementById("keyboard_" + letter), HTMLButtonElement); 211 | if (result.pattern[i] == 0 212 | && keyboard_button.className !== "present" 213 | && keyboard_button.className !== "correct") 214 | keyboard_button.className = "absent"; 215 | if (result.pattern[i] == 1 216 | && keyboard_button.className !== "correct") 217 | keyboard_button.className = "present"; 218 | if (result.pattern[i] == 2 || result.pattern[i] == 3) 219 | keyboard_button.className = "correct"; 220 | 221 | if (i == 0 && result.start) letter_element.classList.add("start"); 222 | if (i == guess.length - 1 && result.end) letter_element.classList.add("end"); 223 | 224 | row.appendChild(letter_element); 225 | }); 226 | $board.insertBefore(row, $inputRow); 227 | 228 | $inputRow.innerHTML = ""; 229 | $inputRow.classList.add("empty"); 230 | } 231 | 232 | function inputLetter(letter: string) { 233 | if (play.history[play.history.length - 1] == play.answer) return; 234 | if (!/^[a-z]$/.test(letter)) throw new Error("invalid input"); 235 | if (10 <= play.guess.length) return; 236 | 237 | insertLetter(letter); 238 | 239 | play.guess += letter; 240 | save(); 241 | } 242 | function inputBackspace() { 243 | if (play.history[play.history.length - 1] == play.answer) return; 244 | if ($inputRow.lastElementChild) $inputRow.removeChild($inputRow.lastElementChild); 245 | if (play.guess !== "") 246 | play.guess = play.guess.substring(0, play.guess.length - 1); 247 | if (play.guess == "") 248 | $inputRow.classList.add("empty"); 249 | 250 | save(); 251 | } 252 | 253 | function enter() { 254 | if (play.history[play.history.length - 1] == play.answer) return; 255 | if (!allowed.includes(play.guess)) { 256 | myAlert("not in word list"); 257 | return; 258 | } 259 | if (play.history.length == 0) { 260 | stats.played++; 261 | showStats(); 262 | } 263 | 264 | insertGuess(play.guess); 265 | 266 | play.letter_count += play.guess.length; 267 | play.history.push(play.guess); 268 | 269 | if (play.guess == play.answer) { 270 | if (play.history.length <= 1) myAlert("miracle!"); 271 | else if (play.history.length <= 3) myAlert("genius!"); 272 | else if (play.history.length <= 6) myAlert("excellent!"); 273 | else if (play.history.length <= 10) myAlert("great!"); 274 | else myAlert("good!"); 275 | stats.won++; 276 | stats.total_guess_count += play.history.length; 277 | stats.total_letter_count += play.letter_count; 278 | showReault(); 279 | showStats(); 280 | } 281 | 282 | play.guess = ""; 283 | 284 | save(); 285 | } 286 | 287 | function showReault() { 288 | $inputRow.style.display = "none"; 289 | assure(document.getElementById("result"), HTMLDivElement).style.display = ""; 290 | assure(document.getElementById("timer_container"), HTMLDivElement).style.display = ""; 291 | assure(document.getElementById("letters_used"), HTMLDivElement).textContent = "" + play.letter_count; 292 | assure(document.getElementById("words_used"), HTMLDivElement).textContent = "" + play.history.length; 293 | assure(document.getElementById("words_used_label"), HTMLSpanElement).innerHTML = play.history.length <= 1 ? "Word
Used" : "Words
Used"; 294 | } 295 | 296 | function showStats() { 297 | assure(document.getElementById("stats_played"), HTMLDivElement).textContent = "" + stats.played; 298 | assure(document.getElementById("stats_won"), HTMLDivElement).textContent = "" + stats.won; 299 | assure(document.getElementById("stats_average_words"), HTMLDivElement).textContent = stats.won == 0 ? "0.0" : (stats.total_guess_count / stats.won).toFixed(1); 300 | assure(document.getElementById("stats_average_letters"), HTMLDivElement).textContent = stats.won == 0 ? "0.0" : (stats.total_letter_count / stats.won).toFixed(1); 301 | } 302 | 303 | function myAlert(message: string) { 304 | const alert = assure(document.getElementById("alert"), HTMLDivElement); 305 | 306 | alert.textContent = message; 307 | alert.classList.add("visible"); 308 | 309 | setTimeout(() => alert.classList.remove("visible"), 1500); 310 | } 311 | 312 | function share() { 313 | const title = "Diffle " + play.date + "\n"; 314 | const result = play.history.length + (play.history.length <= 1 ? " word / " : " words / ") + play.letter_count + " letters\n\n"; 315 | const pattern = play.history.map((x, i) => diffle(play.answer, x).pattern.map(y => 316 | i == play.history.length - 1 ? "\ud83d\udfe9" : y == 0 ? "\u26AA" : y == 1 ? "\ud83d\udfe1" : "\ud83d\udfe2" 317 | ).join("")).join("\n"); 318 | const url = location.href; 319 | 320 | navigator.clipboard.writeText(title + result + pattern + "\n\n" + url).then(function () { 321 | myAlert('Copyed results to clipboard'); 322 | }).catch(function (error) { 323 | myAlert(error.message); 324 | }); 325 | } 326 | 327 | function shareImage() { 328 | const width = 500; 329 | const circle_radius = 21; 330 | const dot_radius = 4; 331 | const margin_x = 2; 332 | const margin_y = 10; 333 | const header_height = 70; 334 | 335 | const canvas = document.createElement("canvas"); 336 | canvas.width = width; 337 | canvas.height = play.history.length * (circle_radius + margin_y) * 2 + header_height; 338 | const context = assure(canvas.getContext("2d"), CanvasRenderingContext2D); 339 | 340 | 341 | context.fillStyle = "#ffffff"; 342 | context.fillRect(0, 0, canvas.width, canvas.height); 343 | context.fillStyle = "#1a1a1b"; 344 | context.font = "40px 'HK Super Round Bold'"; 345 | context.textAlign = "center"; 346 | context.fillText("Diffle".split("").join(String.fromCharCode(8202)), width / 2, 40); 347 | 348 | context.font = "20px 'HK Super Round Bold'"; 349 | context.textAlign = "center"; 350 | context.fillText(play.date, width / 2, 65); 351 | 352 | play.history.forEach((guess, i) => { 353 | const center_y = (2 * i + 1) * (circle_radius + margin_y) + header_height; 354 | const result = diffle(play.answer, guess); 355 | 356 | if (guess == play.answer) { 357 | context.fillStyle = "#4fb061"; 358 | context.fillRect(0, center_y - circle_radius, width, circle_radius * 2); 359 | return; 360 | } 361 | 362 | if (result.start) { 363 | context.fillStyle = "#4fb061"; 364 | context.fillRect(width / 2 - guess.length * (circle_radius + margin_x), center_y - circle_radius, circle_radius + margin_x, circle_radius * 2); 365 | } 366 | if (result.end) { 367 | context.fillStyle = "#4fb061"; 368 | context.fillRect(width / 2 + (guess.length - 1) * (circle_radius + margin_x), center_y - circle_radius, circle_radius + margin_x, circle_radius * 2); 369 | } 370 | 371 | result.pattern.forEach((color, j) => { 372 | const center_x = width / 2 + (1 + 2 * j - guess.length) * (circle_radius + margin_x); 373 | 374 | context.beginPath(); 375 | context.arc(center_x, center_y, circle_radius, 0, 360 * Math.PI / 180, false); 376 | context.fillStyle = ["#959b9d", "#e8b838", "#4fb061", "#4fb061"][color]; 377 | context.fill(); 378 | 379 | if (color == 3) { 380 | context.fillStyle = "#4fb061"; 381 | context.fillRect(center_x - (circle_radius + margin_x) * 2, center_y - circle_radius, (circle_radius + margin_x) * 2, circle_radius * 2); 382 | context.fill(); 383 | } 384 | }); 385 | }); 386 | 387 | canvas.toBlob(blob => { 388 | try { 389 | if (blob == null) { 390 | throw new Error("something went wrong"); 391 | } 392 | var fileURL = URL.createObjectURL(blob); 393 | window.open(fileURL); 394 | } catch (err) { 395 | console.log(err); 396 | } 397 | }, "image/png"); 398 | } 399 | 400 | document.addEventListener("keydown", (ev) => { 401 | if (ev.key == "Backspace") inputBackspace(); 402 | if (ev.key == "Enter") enter(); 403 | if (/^[A-Za-z]$/.test(ev.key)) inputLetter(ev.key.toLowerCase()); 404 | }); 405 | 406 | Array.from("qwertyuiopasdfghjklzxcvbnm").forEach(letter => { 407 | const keyboard_button = assure(document.getElementById("keyboard_" + letter), HTMLButtonElement); 408 | keyboard_button.addEventListener("click", () => inputLetter(letter)); 409 | }); 410 | 411 | function getTodayString() { 412 | const now = new Date(); 413 | return `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`; 414 | } 415 | 416 | function updateTimer() { 417 | const today = getTodayString(); 418 | if (play.date !== today) { 419 | assure(document.getElementById("timer"), HTMLDivElement).textContent = "00:00:00"; 420 | return; 421 | } 422 | 423 | const now = new Date(); 424 | const rest = 86400 - (3600 * now.getHours() + 60 * now.getMinutes() + now.getSeconds()); 425 | 426 | const rest_hours = Math.floor(rest / 3600); 427 | const rest_minutes = Math.floor((rest - 3600 * rest_hours) / 60); 428 | const rest_seconds = rest - 3600 * rest_hours - 60 * rest_minutes; 429 | const rest_format = `${("" + rest_hours).padStart(2, "0")}:${("" + rest_minutes).padStart(2, "0")}:${("" + rest_seconds).padStart(2, "0")}`; 430 | 431 | assure(document.getElementById("timer"), HTMLDivElement).textContent = rest_format; 432 | } 433 | 434 | assure(document.getElementById("keyboard_enter"), HTMLButtonElement).addEventListener("click", enter); 435 | assure(document.getElementById("keyboard_backspace"), HTMLButtonElement).addEventListener("click", inputBackspace); 436 | assure(document.getElementById("share_button"), HTMLButtonElement).addEventListener("click", share); 437 | assure(document.getElementById("share_image_button"), HTMLButtonElement).addEventListener("click", shareImage); 438 | 439 | load(); 440 | setInterval(updateTimer, 1000); --------------------------------------------------------------------------------