├── 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 |
37 |
38 |
40 |
44 |
45 |
46 |
47 |
Letters
Used
48 |
49 |
50 |
51 |
59 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
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 |
213 |
217 |
218 |
219 |
Average
Words
220 |
221 |
222 |
223 |
Average
Letters
224 |
225 |
226 |
227 |
Next DIFFLE
228 |
229 |
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);
--------------------------------------------------------------------------------