├── README.md
├── src
├── react-app-env.d.ts
├── images
│ ├── icon.png
│ ├── how_to_play_1.gif
│ ├── how_to_play_2.gif
│ └── how_to_play_3.png
├── setupTests.ts
├── index.css
├── reportWebVitals.ts
├── hooks
│ ├── useInventoryDrag.ts
│ ├── usePickupMino.ts
│ └── useDropMino.ts
├── index.tsx
├── utils
│ ├── function.ts
│ └── random.ts
├── components
│ ├── Laser.tsx
│ ├── PortraitInventory.tsx
│ ├── LandscapeInventory.tsx
│ ├── Timer.tsx
│ ├── Cell.tsx
│ ├── StartPoint.tsx
│ ├── BoardMino.tsx
│ ├── EndPoint.tsx
│ ├── InventoryMino.tsx
│ ├── OverlayMino.tsx
│ ├── Board.tsx
│ ├── Canvas.tsx
│ └── ReflecMino.tsx
├── theme
│ ├── gh_dark.ts
│ └── yv_dark.ts
└── puzzle
│ ├── simulate_laser.ts
│ ├── decode.ts
│ ├── const.ts
│ └── generate.ts
├── public
├── icon.png
├── favicon.ico
├── robots.txt
├── thumbnail.png
├── manifest.json
└── index.html
├── .gitignore
├── tsconfig.json
├── LICENSE
└── package.json
/README.md:
--------------------------------------------------------------------------------
1 | [ReflecMino](https://yavu.github.io/yv_reflecmino/)
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/public/icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/src/images/icon.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/public/thumbnail.png
--------------------------------------------------------------------------------
/src/images/how_to_play_1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/src/images/how_to_play_1.gif
--------------------------------------------------------------------------------
/src/images/how_to_play_2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/src/images/how_to_play_2.gif
--------------------------------------------------------------------------------
/src/images/how_to_play_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yavu/yv_reflecmino/HEAD/src/images/how_to_play_3.png
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "ReflecMino",
3 | "name": "ReflecMino",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "256x256 180x180 128x128 64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "icon.png",
12 | "type": "image/png",
13 | "sizes": "256x256"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/hooks/useInventoryDrag.ts:
--------------------------------------------------------------------------------
1 | import { KonvaEventObject } from "konva/lib/Node";
2 | import { useCallback } from "react";
3 |
4 | const useInventoryDrag = (width: number) => {
5 | return useCallback(
6 | (e: KonvaEventObject) => {
7 | e.target.y(338);
8 | if (width >= 656 || e.target.x() > 0) {
9 | e.target.x(0);
10 | }
11 | else if (e.target.x() < width - 656) {
12 | e.target.x(width - 656);
13 | }
14 | }, [width]
15 | );
16 | }
17 |
18 | export default useInventoryDrag;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import reportWebVitals from './reportWebVitals';
5 | import ReflecMino from './components/ReflecMino';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/src/utils/function.ts:
--------------------------------------------------------------------------------
1 | export function compose_n(n: number, f: (a: A) => A): (a: A) => A {
2 | return [...Array(n - 1)].reduce((a) => (x: A) => f(a(x)), f);
3 | };
4 |
5 | export const while_f = (a: A, f: (a: A) => [cont: boolean, a: A]): A => {
6 | let cont: boolean;
7 | do { [cont, a] = f(a); } while (cont);
8 | return a;
9 | };
10 |
11 | export const replace_array = (base: A[], index: number, other: A) => [...base.slice(0, index), other, ...base.slice(index + 1)];
12 |
13 | export const replace_2d_array = (base: A[][], x: number, y: number, other: A) => {
14 | const y_array = replace_array(base[y], x, other);
15 | return replace_array(structuredClone(base), y, y_array);
16 | };
17 |
18 | export const is_invalid_date = (date: Date) => Number.isNaN(date.getTime());
--------------------------------------------------------------------------------
/src/utils/random.ts:
--------------------------------------------------------------------------------
1 | // これをimmutableにするのはTypeScriptではつらいので今回は諦める
2 | export class random {
3 | x: number;
4 | y: number;
5 | z: number;
6 | w: number;
7 | constructor(seed = 88675123) {
8 | this.x = 123456789;
9 | this.y = 362436069;
10 | this.z = 521288629;
11 | this.w = seed;
12 | }
13 |
14 | // XorShift
15 | next() {
16 | let t = this.x ^ (this.x << 11);
17 | this.x = this.y; this.y = this.z; this.z = this.w;
18 | return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
19 | }
20 |
21 | // min以上max以下の乱数を生成する
22 | next_int(min: number, max: number) {
23 | const r = Math.abs(this.next());
24 | return min + (r % (max - min));
25 | }
26 |
27 | // boolの乱数を生成する
28 | next_bool() {
29 | const r = Math.abs(this.next());
30 | return !!(0 + (r % (2 - 0)));
31 | }
32 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 yavu
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 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ReflecMino
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/components/Laser.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Group, Line } from "react-konva";
3 | import { LaserData } from "../puzzle/const";
4 |
5 | type LaserProp = {
6 | index: number,
7 | laser_data: LaserData[]
8 | };
9 |
10 | const Laser = ({ index, laser_data }: LaserProp): JSX.Element => {
11 | const color = index === 0 ? "#0099ff" : "#ff801e";
12 | const same = `${laser_data[0].board}` === `${laser_data[1].board}`;
13 | return (
14 | <>
15 |
21 |
27 |
33 |
34 | >
35 | );
36 | }
37 |
38 | export default React.memo(Laser);
39 | // export default Laser;
--------------------------------------------------------------------------------
/src/components/PortraitInventory.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Rect } from "react-konva";
2 | import React from "react";
3 |
4 | type Prop = {
5 | x: number,
6 | y: number,
7 | visible: boolean
8 | }
9 |
10 | const PortraitInventory = ({ x, y, visible }: Prop): JSX.Element => {
11 | return (
12 |
17 |
27 |
37 |
47 |
57 |
58 | );
59 | }
60 |
61 | export default React.memo(PortraitInventory);
--------------------------------------------------------------------------------
/src/components/LandscapeInventory.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Rect } from "react-konva";
2 | import React from "react";
3 |
4 | type Prop = {
5 | x: number,
6 | y: number,
7 | visible: boolean
8 | }
9 |
10 | const LandscapeInventory = ({ x, y, visible }: Prop): JSX.Element => {
11 | return (
12 |
17 |
27 |
37 |
47 |
57 |
58 | );
59 | }
60 |
61 | export default React.memo(LandscapeInventory);
--------------------------------------------------------------------------------
/src/components/Timer.tsx:
--------------------------------------------------------------------------------
1 | import { Theme, Typography } from '@mui/material';
2 | import React from 'react';
3 |
4 | type TimerProp = {
5 | enabled: boolean,
6 | theme: Theme,
7 | solved: boolean,
8 | playing: boolean
9 | }
10 |
11 | const Timer = ({ enabled, theme, solved, playing }: TimerProp): JSX.Element => {
12 |
13 | const [time, setTime] = React.useState(0);
14 |
15 | React.useEffect(() => {
16 | if (enabled) {
17 | const id = setInterval(() => {
18 | setTime(t => t < 5999 ? t + 1 : t);
19 | }, 1000);
20 | return () => clearInterval(id);
21 | }
22 | else if (!playing && !solved) {
23 | setTime(0);
24 | }
25 | }, [enabled, playing, solved]);
26 |
27 | const m = Math.floor(time / 60);
28 | const s = time % 60;
29 |
30 | return (
31 |
50 | {m < 10 ? "0" : ""}{m}:{s < 10 ? "0" : ""}{s}
51 |
52 | );
53 | };
54 |
55 | export default React.memo(Timer);
--------------------------------------------------------------------------------
/src/components/Cell.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Rect, Line } from 'react-konva';
3 |
4 | type CellProp = {
5 | data: { x: number; y: number; type: string; },
6 | color: { fill: string, stroke: string } | undefined,
7 | rect_visible: boolean
8 | };
9 | const Cell = ({ data, color, rect_visible }: CellProp): JSX.Element => {
10 | const rect_props: Parameters[0] = {
11 | width: 34,
12 | height: 34,
13 | x: 8 + 50 * data.x,
14 | y: 8 + 50 * data.y,
15 | fill: color ? color.fill : "#9ba5ad",
16 | stroke: color ? color.stroke : "#828c94",
17 | strokeWidth: 4,
18 | lineJoin: "round",
19 | visible: rect_visible
20 | };
21 | switch (data.type) {
22 | case "/":
23 | return (
24 | <>
25 |
26 |
34 | >
35 | );
36 | case "\\":
37 | return (
38 | <>
39 |
40 |
48 | >
49 | );
50 | default:
51 | return (
52 |
53 | );
54 | }
55 | };
56 |
57 | export default React.memo(Cell);
58 | // export default Cell;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yv_reflecmino",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.1",
7 | "@emotion/styled": "^11.11.0",
8 | "@mui/icons-material": "^5.15.0",
9 | "@mui/material": "^5.14.10",
10 | "@mui/x-date-pickers": "^6.18.5",
11 | "@testing-library/jest-dom": "^5.17.0",
12 | "@testing-library/react": "^13.4.0",
13 | "@testing-library/user-event": "^13.5.0",
14 | "@types/jest": "^27.5.2",
15 | "@types/node": "^16.18.54",
16 | "@types/react": "^18.2.22",
17 | "@types/react-dom": "^18.2.7",
18 | "@typescript-eslint/eslint-plugin": "^5.62.0",
19 | "@typescript-eslint/parser": "^5.62.0",
20 | "date-fns": "^2.30.0",
21 | "eslint": "^8.55.0",
22 | "eslint-plugin-react": "^7.33.2",
23 | "eslint-plugin-react-hooks": "^4.6.0",
24 | "gh-pages": "^6.1.0",
25 | "konva": "^9.2.3",
26 | "react": "^18.2.0",
27 | "react-dom": "^18.2.0",
28 | "react-konva": "^18.2.10",
29 | "react-konva-utils": "^1.0.5",
30 | "react-measure": "^2.5.2",
31 | "react-scripts": "5.0.1",
32 | "typescript": "^4.9.5",
33 | "web-vitals": "^2.1.4"
34 | },
35 | "homepage": "https://yavu.github.io/yv_reflecmino/",
36 | "scripts": {
37 | "start": "react-scripts start",
38 | "build": "react-scripts build",
39 | "test": "react-scripts test",
40 | "predeploy": "npm run build",
41 | "deploy": "gh-pages -d build",
42 | "eject": "react-scripts eject"
43 | },
44 | "eslintConfig": {
45 | "extends": [
46 | "react-app",
47 | "react-app/jest"
48 | ]
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "@types/react-measure": "^2.0.12"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/StartPoint.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Group, Line, Rect } from "react-konva";
3 |
4 | type StartPointProp = {
5 | pos: { x: number; y: number; },
6 | color: { fill: string, stroke: string }
7 | };
8 |
9 | const StartPoint = ({ pos, color }: StartPointProp): JSX.Element => {
10 | const laser_vertex = (() => {
11 | if (pos.x === 0) { return [0, 0, 25, 0]; }
12 | else if (pos.x === 6) { return [0, 0, -25, 0]; }
13 | else if (pos.y === 0) { return [0, 0, 0, 25]; }
14 | else { return [0, 0, 0, -25]; }
15 | })();
16 |
17 | return (
18 |
22 |
30 |
38 |
48 |
55 |
63 |
64 | );
65 | }
66 |
67 | export default React.memo(StartPoint);
68 | // export default StartPoint;
--------------------------------------------------------------------------------
/src/hooks/usePickupMino.ts:
--------------------------------------------------------------------------------
1 | import { KonvaEventObject } from "konva/lib/Node";
2 | import { useCallback } from "react";
3 | import { replace_2d_array } from "../utils/function";
4 | import { PuzzleData } from "../puzzle/const";
5 | import { simulate_laser } from "../puzzle/simulate_laser";
6 |
7 | const usePickupMino = (index: number, setPuzzleData: React.Dispatch>, setDraggingMinoIndex: React.Dispatch>) => {
8 | return useCallback(
9 | (e: KonvaEventObject) => {
10 | e.cancelBubble = true;
11 | setDraggingMinoIndex(index);
12 | setPuzzleData((prev_data) => {
13 | const picked_mino = prev_data[1][index];
14 | const new_board = (() => {
15 | if (picked_mino.pos) {
16 | const place_1 = replace_2d_array([...prev_data[0]], picked_mino.pos.x + picked_mino.cell[0].x, picked_mino.pos.y + picked_mino.cell[0].y, " ");
17 | const place_2 = replace_2d_array(place_1, picked_mino.pos.x + picked_mino.cell[1].x, picked_mino.pos.y + picked_mino.cell[1].y, " ");
18 | return replace_2d_array(place_2, picked_mino.pos.x + picked_mino.cell[2].x, picked_mino.pos.y + picked_mino.cell[2].y, " ");
19 | }
20 | else {
21 | return prev_data[0];
22 | }
23 | })();
24 | const new_laser = [
25 | simulate_laser(new_board, prev_data[2][0].start),
26 | simulate_laser(new_board, prev_data[2][1].start)
27 | ];
28 |
29 | return [
30 | new_board,
31 | prev_data[1],
32 | [
33 | { ...prev_data[2][0], board: new_laser[0][0], vertex: new_laser[0][4] },
34 | { ...prev_data[2][1], board: new_laser[1][0], vertex: new_laser[1][4] }
35 | ]
36 | ]
37 | });
38 | }, [index, setPuzzleData, setDraggingMinoIndex]
39 | );
40 | }
41 |
42 | export default usePickupMino;
--------------------------------------------------------------------------------
/src/theme/gh_dark.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from '@mui/material/styles';
2 |
3 | export const gh_dark = createTheme({
4 | typography: {
5 | fontFamily: "Share Tech Mono",
6 | button: {
7 | fontWeight: 600,
8 | },
9 | },
10 | palette: {
11 | mode: "dark",
12 | primary: {
13 | main: "#00b0ff",
14 | contrastText: "#2a3648",
15 | },
16 | secondary: {
17 | main: "#ef6c00",
18 | },
19 | background: {
20 | default: '#22272e',
21 | paper: '#1b2029',
22 | },
23 | error: {
24 | main: "#ff1744",
25 | },
26 | },
27 | spacing: 16,
28 | components: {
29 | MuiCssBaseline: {
30 | styleOverrides: `
31 | ::-webkit-scrollbar {
32 | width: 8px;
33 | height: 8px;
34 | }
35 |
36 | ::-webkit-scrollbar-track {
37 | background: transparent;
38 | }
39 |
40 | ::-webkit-scrollbar-thumb {
41 | background: rgba(136, 153, 185, 0.3);
42 | border-radius: 8px;
43 | opacity: 0.1;
44 | }
45 | @font-face {
46 | font-family: 'Share Tech Mono';
47 | font-style: normal;
48 | font-weight: 400;
49 | font-display: swap;
50 | src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
51 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
52 | }
53 | `
54 | },
55 | MuiButton: {
56 | styleOverrides: {
57 | root: {
58 | boxSizing: "border-box",
59 | height: "40px",
60 | paddingTop: "8px",
61 | }
62 | }
63 | },
64 | MuiToggleButtonGroup: {
65 | styleOverrides: {
66 | root: {
67 | boxSizing: "border-box",
68 | height: "40px"
69 | }
70 | }
71 | }
72 | }
73 | });
74 |
--------------------------------------------------------------------------------
/src/theme/yv_dark.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from '@mui/material/styles';
2 |
3 | export const yv_dark = createTheme({
4 | typography: {
5 | fontFamily: "Share Tech Mono",
6 | button: {
7 | fontWeight: 600,
8 | },
9 | },
10 | palette: {
11 | mode: "dark",
12 | primary: {
13 | main: "#00b0ff",
14 | contrastText: "#2a3648",
15 | },
16 | secondary: {
17 | main: "#ef6c00",
18 | },
19 | background: {
20 | default: "#101d31",
21 | paper: "#101d31",
22 | },
23 | error: {
24 | main: "#ff1744",
25 | },
26 | },
27 | spacing: 16,
28 | components: {
29 | MuiCssBaseline: {
30 | styleOverrides: `
31 | ::-webkit-scrollbar {
32 | width: 8px;
33 | height: 8px;
34 | }
35 |
36 | ::-webkit-scrollbar-track {
37 | background: transparent;
38 | }
39 |
40 | ::-webkit-scrollbar-thumb {
41 | background: rgba(30, 160, 255, 0.3);
42 | border-radius: 8px;
43 | opacity: 0.1;
44 | }
45 | @font-face {
46 | font-family: 'Share Tech Mono';
47 | font-style: normal;
48 | font-weight: 400;
49 | font-display: swap;
50 | src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
51 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
52 | }
53 | `
54 | },
55 | MuiButton: {
56 | styleOverrides: {
57 | root: ({ ownerState }) => ({
58 | ...(ownerState.size === "medium" && {
59 | boxSizing: "border-box",
60 | height: "40px",
61 | paddingTop: "8px",
62 | })
63 | })
64 | }
65 | },
66 | MuiToggleButtonGroup: {
67 | styleOverrides: {
68 | root: {
69 | boxSizing: "border-box",
70 | height: "40px"
71 | }
72 | }
73 | }
74 | }
75 | });
76 |
--------------------------------------------------------------------------------
/src/puzzle/simulate_laser.ts:
--------------------------------------------------------------------------------
1 | import { replace_2d_array, while_f } from "../utils/function";
2 | import { empty_board } from "./const";
3 |
4 | export function simulate_laser(board: string[][], start_pos: { x: number, y: number }) {
5 | type Move = [0, 1] | [0, -1] | [1, 0] | [-1, 0];
6 | type Laser = [board: string[][], x: number, y: number, move: Move, vertex: number[]];
7 | const move = (data: Laser): Laser => {
8 | const x = data[1] + data[3][0];
9 | const y = data[2] + data[3][1];
10 | const laser_board = replace_2d_array(data[0], x, y, "■");
11 | const move = (() => {
12 | const reflection = (direction: boolean, move: Move): Move => {
13 | if (direction) {
14 | switch (move[1]) {
15 | case 0: return [0, move[0]];
16 | case 1: return [-1, 0];
17 | case -1: return [1, 0];
18 | // [a, b] => [-b, a] 右折
19 | }
20 | }
21 | else {
22 | switch (move[0]) {
23 | case 0: return [move[1], 0];
24 | case 1: return [0, -1];
25 | case -1: return [0, 1];
26 | // [a, b] => [b, -a] 左折
27 | }
28 | }
29 | };
30 | if (board[y][x] === "/") {
31 | return reflection(data[3][0] === 0, data[3])
32 | }
33 | else if (board[y][x] === "\\") {
34 | return reflection(data[3][0] !== 0, data[3])
35 | }
36 | else {
37 | return data[3];
38 | }
39 | })();
40 | return [laser_board, x, y, move, [...data[4], x * 50 - 25, y * 50 - 25]];
41 | }
42 | const initial: Laser = [
43 | replace_2d_array(empty_board, start_pos.x, start_pos.y, "■"),
44 | start_pos.x,
45 | start_pos.y,
46 | (() => {
47 | if (start_pos.x === 0) { return [1, 0]; }
48 | else if (start_pos.x === 6) { return [-1, 0]; }
49 | else if (start_pos.y === 0) { return [0, 1]; }
50 | else { return [0, -1]; }
51 | })(),
52 | [start_pos.x * 50 - 25, start_pos.y * 50 - 25]
53 | ];
54 | return while_f(initial, s => {
55 | const result = move(s);
56 | return [0 < result[1] && result[1] < 6 && 0 < result[2] && result[2] < 6, result];
57 | });
58 | }
--------------------------------------------------------------------------------
/src/components/BoardMino.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { LaserData, PuzzleData } from "../puzzle/const";
3 | import { Group, Line } from 'react-konva';
4 | import Cell from './Cell';
5 |
6 | type BoardMinoProp = {
7 | index: number,
8 | puzzle_data: PuzzleData,
9 | dragging_mino_index: number | undefined
10 | };
11 |
12 | const BoardMino = ({ index, puzzle_data, dragging_mino_index }: BoardMinoProp): JSX.Element => {
13 | const mino = puzzle_data[1][index];
14 | const pos = mino.pos
15 | ? {
16 | x: (mino.pos.x - 1) * 50 + 25,
17 | y: (mino.pos.y - 1) * 50 + 25
18 | }
19 | : undefined;
20 | const get_cell_color = (pos: { x: number, y: number } | undefined, x: number, y: number, laser_data: LaserData[]) => {
21 | if (pos) {
22 | const blue = laser_data[0].board[pos.y + y][pos.x + x] === "■";
23 | const orange = laser_data[1].board[pos.y + y][pos.x + x] === "■";
24 | if (blue && orange) {
25 | return { fill: "#ffffff", stroke: "#dddddd" };
26 | }
27 | else if (blue) {
28 | return { fill: "#14b3ff", stroke: "#0099ff" };
29 | }
30 | else if (orange) {
31 | return { fill: "#fe9f56", stroke: "#ff801e" };
32 | }
33 | else {
34 | return { fill: "#9ba5ad", stroke: "#828c94" };
35 | }
36 | }
37 | else {
38 | return { fill: "#9ba5ad", stroke: "#828c94" };
39 | }
40 | }
41 | return (
42 |
49 |
57 | |
58 | |
59 | |
60 |
61 | );
62 | }
63 |
64 | export default React.memo(BoardMino);
65 | // export default BoardMino;
--------------------------------------------------------------------------------
/src/components/EndPoint.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Line, Rect } from "react-konva";
2 | import { LaserData } from "../puzzle/const";
3 | import React from "react";
4 |
5 | type EndPointProp = {
6 | pos: { x: number; y: number; },
7 | laser_data: LaserData[]
8 | };
9 |
10 | const EndPoint = ({ pos, laser_data }: EndPointProp): JSX.Element => {
11 | const blue = laser_data[0].board[pos.y][pos.x] === "■";
12 | const orange = laser_data[1].board[pos.y][pos.x] === "■";
13 | const color = (() => {
14 | if (blue && orange) {
15 | return { fill: "#ffffff", stroke: "#dddddd" };
16 | }
17 | else if (blue) {
18 | return { fill: "#14b3ff", stroke: "#0099ff" };
19 | }
20 | else if (orange) {
21 | return { fill: "#fe9f56", stroke: "#ff801e" };
22 | }
23 | else {
24 | return { fill: "#9ba5ad", stroke: "#828c94" };
25 | }
26 | })();
27 | const laser_vertex = (() => {
28 | if (blue || orange) {
29 | if (pos.x === 0) { return [0, 0, 25, 0]; }
30 | else if (pos.x === 6) { return [0, 0, -25, 0]; }
31 | else if (pos.y === 0) { return [0, 0, 0, 25]; }
32 | else { return [0, 0, 0, -25]; }
33 | }
34 | else {
35 | return [];
36 | }
37 | })();
38 |
39 | return (
40 |
44 |
52 |
60 |
69 |
76 |
84 |
85 | );
86 | }
87 |
88 | export default React.memo(EndPoint);
89 | // export default EndPoint;
--------------------------------------------------------------------------------
/src/components/InventoryMino.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { PuzzleData } from "../puzzle/const";
3 | import { KonvaEventObject } from 'konva/lib/Node';
4 | import { Group, Line } from 'react-konva';
5 | import Cell from './Cell';
6 | import usePickupMino from '../hooks/usePickupMino';
7 | import useDropMino from '../hooks/useDropMino';
8 |
9 | type InventoryMinoProp = {
10 | index: number,
11 | x: number,
12 | y: number,
13 | scale: { x: number, y: number },
14 | puzzle_data: PuzzleData,
15 | setPuzzleData: React.Dispatch>,
16 | dragging_mino_index: number | undefined,
17 | setDraggingMinoIndex: React.Dispatch>,
18 | setSolved: React.Dispatch>
19 | };
20 |
21 | const InventoryMino = ({ index, x, y, scale, puzzle_data, setPuzzleData, dragging_mino_index, setDraggingMinoIndex, setSolved }: InventoryMinoProp): JSX.Element => {
22 | const picked_mino = puzzle_data[1][index];
23 | const onDragStart = usePickupMino(index, setPuzzleData, setDraggingMinoIndex);
24 | const centered_pos = {
25 | x: x - (picked_mino.cell[0].x + picked_mino.cell[1].x + picked_mino.cell[2].x) * 25 * scale.x,
26 | y: y - (picked_mino.cell[0].y + picked_mino.cell[1].y + picked_mino.cell[2].y) * 25 * scale.y
27 | };
28 | const onDragMove = useCallback(
29 | (e: KonvaEventObject) => {
30 | e.cancelBubble = true;
31 | e.target.scale({ x: 1, y: 1 });
32 | }, []
33 | );
34 | const onDragEnd = useDropMino(index, setPuzzleData, setDraggingMinoIndex, centered_pos, scale);
35 |
36 | const non_activated_cells = [...puzzle_data[0]].map((y, y_index) => y.map((e, x_index) => (e !== "#" && e !== " " && puzzle_data[2][0].board[y_index][x_index] !== "■" && puzzle_data[2][1].board[y_index][x_index] !== "■") ? "■" : " "));
37 | setSolved(
38 | !non_activated_cells.flat().includes("■") &&
39 | puzzle_data[1][0].pos !== undefined &&
40 | puzzle_data[1][1].pos !== undefined &&
41 | puzzle_data[1][2].pos !== undefined &&
42 | puzzle_data[1][3].pos !== undefined &&
43 | dragging_mino_index === undefined
44 | );
45 |
46 | return (
47 |
58 |
66 | |
67 | |
68 | |
69 |
70 | );
71 | }
72 |
73 | export default React.memo(InventoryMino);
74 | // export default InventoryMino;
--------------------------------------------------------------------------------
/src/puzzle/decode.ts:
--------------------------------------------------------------------------------
1 | import { replace_2d_array } from "../utils/function";
2 | import { PuzzleData, decode_table, empty_board } from "./const";
3 | import { simulate_laser } from "./simulate_laser";
4 |
5 | export function decode(code: string): PuzzleData | undefined {
6 | const chars = code
7 | ? code.split("")
8 | : [];
9 | const cells = [...chars.slice(0, 3), ...chars.slice(4, 7), ...chars.slice(8, 11), ...chars.slice(12, 15)].map(e => decode_table.cell.find(([k]) => (k === e))?.[1]).filter((e): e is Exclude => e !== undefined);
10 | const vertices = [chars[3], chars[7], chars[11], chars[15]].map(e => decode_table.vertex.find(([k,]) => (k === e))?.[1]).filter((e): e is Exclude => e !== undefined);
11 | const pos = [...chars.slice(16)].map(e => decode_table.pos.find(([k,]) => (k === e))?.[1]).filter((e): e is Exclude => e !== undefined);
12 |
13 | if (chars.length === 20 && cells.length === 12 && vertices.length === 4 && pos.length === 4) {
14 | const laser_board = [
15 | simulate_laser(empty_board, pos[0]),
16 | simulate_laser(empty_board, pos[2])
17 | ];
18 | const board = (() => {
19 | const draw_1 = replace_2d_array(empty_board, pos[0].x, pos[0].y, "s");
20 | const draw_2 = replace_2d_array(draw_1, pos[1].x, pos[1].y, "e");
21 | const draw_3 = replace_2d_array(draw_2, pos[2].x, pos[2].y, "s");
22 | return replace_2d_array(draw_3, pos[3].x, pos[3].y, "e");
23 | })();
24 |
25 | return [
26 | board,
27 | [
28 | {
29 | cell: [
30 | cells[0],
31 | cells[1],
32 | cells[2],
33 | ],
34 | pos: undefined,
35 | vertex: vertices[0]
36 | },
37 | {
38 | cell: [
39 | cells[3],
40 | cells[4],
41 | cells[5],
42 | ],
43 | pos: undefined,
44 | vertex: vertices[1]
45 | },
46 | {
47 | cell: [
48 | cells[6],
49 | cells[7],
50 | cells[8],
51 | ],
52 | pos: undefined,
53 | vertex: vertices[2]
54 | },
55 | {
56 | cell: [
57 | cells[9],
58 | cells[10],
59 | cells[11],
60 | ],
61 | pos: undefined,
62 | vertex: vertices[3]
63 | }
64 | ],
65 | [
66 | {
67 | start: pos[0],
68 | end: pos[1],
69 | board: laser_board[0][0],
70 | vertex: laser_board[0][4]
71 | },
72 | {
73 | start: pos[2],
74 | end: pos[3],
75 | board: laser_board[1][0],
76 | vertex: laser_board[1][4]
77 | }
78 | ]
79 | ]
80 | }
81 | else {
82 | return undefined;
83 | }
84 | }
--------------------------------------------------------------------------------
/src/components/OverlayMino.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { PuzzleData } from "../puzzle/const";
3 | import { KonvaEventObject } from 'konva/lib/Node';
4 | import { Group, Line } from 'react-konva';
5 | import Cell from './Cell';
6 | import usePickupMino from '../hooks/usePickupMino';
7 | import useDropMino from '../hooks/useDropMino';
8 | import { Portal } from 'react-konva-utils';
9 |
10 | type OverlayMinoProp = {
11 | index: number,
12 | puzzle_data: PuzzleData,
13 | setPuzzleData: React.Dispatch>,
14 | dragging_mino_index: number | undefined,
15 | setDraggingMinoIndex: React.Dispatch>
16 | setSolved: React.Dispatch>
17 | };
18 |
19 | const OverlayMino = ({ index, puzzle_data, setPuzzleData, dragging_mino_index, setDraggingMinoIndex, setSolved }: OverlayMinoProp): JSX.Element => {
20 | const picked_mino = puzzle_data[1][index];
21 | const onDragStart = usePickupMino(index, setPuzzleData, setDraggingMinoIndex);
22 | const pos = picked_mino.pos
23 | ? {
24 | x: (picked_mino.pos.x - 1) * 50 + 25,
25 | y: (picked_mino.pos.y - 1) * 50 + 25
26 | }
27 | : undefined;
28 | const onDragMove = useCallback((e: KonvaEventObject) => e.cancelBubble = true, []);
29 | const onDragEnd = useDropMino(index, setPuzzleData, setDraggingMinoIndex, pos, undefined);
30 |
31 | const non_activated_cells = [...puzzle_data[0]].map((y, y_index) => y.map((e, x_index) => (e !== "#" && e !== " " && puzzle_data[2][0].board[y_index][x_index] !== "■" && puzzle_data[2][1].board[y_index][x_index] !== "■") ? "■" : " "));
32 | setSolved(
33 | !non_activated_cells.flat().includes("■") &&
34 | puzzle_data[1][0].pos !== undefined &&
35 | puzzle_data[1][1].pos !== undefined &&
36 | puzzle_data[1][2].pos !== undefined &&
37 | puzzle_data[1][3].pos !== undefined &&
38 | dragging_mino_index === undefined
39 | );
40 |
41 | return (
42 |
46 |
56 |
65 | |
66 | |
67 | |
68 |
69 |
70 | );
71 | }
72 |
73 | export default React.memo(OverlayMino);
74 | // export default BoardMino;
--------------------------------------------------------------------------------
/src/components/Board.tsx:
--------------------------------------------------------------------------------
1 | import { Group, Line, Rect } from "react-konva";
2 | import React from "react";
3 |
4 |
5 |
6 | type BoardProp = {
7 | children: JSX.Element
8 | }
9 |
10 | const Board = ({ children }: BoardProp): JSX.Element => {
11 | return (
12 | <>
13 |
24 |
31 | {children}
32 |
33 |
44 |
52 |
60 |
68 |
76 |
84 |
92 |
100 |
108 | >
109 | );
110 | }
111 |
112 | export default React.memo(Board);
113 | // export default Board;
--------------------------------------------------------------------------------
/src/hooks/useDropMino.ts:
--------------------------------------------------------------------------------
1 | import { KonvaEventObject } from "konva/lib/Node";
2 | import { useCallback } from "react";
3 | import { replace_2d_array } from "../utils/function";
4 | import { PuzzleData } from "../puzzle/const";
5 | import { simulate_laser } from "../puzzle/simulate_laser";
6 |
7 | const useDropMino = (index: number, setPuzzleData: React.Dispatch>, setDraggingMinoIndex: React.Dispatch>, update_pos: { x: number, y: number } | undefined, update_scale: { x: number, y: number } | undefined) => {
8 | return useCallback(
9 | (e: KonvaEventObject) => {
10 | e.cancelBubble = true;
11 | setDraggingMinoIndex(undefined);
12 | setPuzzleData((prev_data) => {
13 | const mino_pos = {
14 | x: Math.round((e.target.x() + 25) / 50),
15 | y: Math.round((e.target.y() + 25) / 50)
16 | };
17 | // console.log(`pos | ${e.target.x()} : ${e.target.y()}`);
18 | // console.log(`offset | ${offset.x} : ${offset.y}`);
19 | // console.log(`mino p | ${Math.round((e.target.x() + drop_offset_x + 25) / 50)} : ${Math.round((e.target.y() + 25) / 50)}`);
20 | const picked_mino = prev_data[1][index];
21 | const cell_pos = [
22 | { x: mino_pos.x + picked_mino.cell[0].x, y: mino_pos.y + picked_mino.cell[0].y },
23 | { x: mino_pos.x + picked_mino.cell[1].x, y: mino_pos.y + picked_mino.cell[1].y },
24 | { x: mino_pos.x + picked_mino.cell[2].x, y: mino_pos.y + picked_mino.cell[2].y }
25 | ];
26 | const on_board = (
27 | 0 < cell_pos[0].x && cell_pos[0].x < 6 && 0 < cell_pos[0].y && cell_pos[0].y < 6 &&
28 | 0 < cell_pos[1].x && cell_pos[1].x < 6 && 0 < cell_pos[1].y && cell_pos[1].y < 6 &&
29 | 0 < cell_pos[2].x && cell_pos[2].x < 6 && 0 < cell_pos[2].y && cell_pos[2].y < 6
30 | );
31 | const placeable = on_board
32 | ? (
33 | prev_data[0][cell_pos[0].y][cell_pos[0].x] === " " &&
34 | prev_data[0][cell_pos[1].y][cell_pos[1].x] === " " &&
35 | prev_data[0][cell_pos[2].y][cell_pos[2].x] === " "
36 | )
37 | : false;
38 | const new_board = (() => {
39 | if (placeable) {
40 | const place_1 = replace_2d_array([...prev_data[0]], mino_pos.x + picked_mino.cell[0].x, mino_pos.y + picked_mino.cell[0].y, picked_mino.cell[0].type);
41 | const place_2 = replace_2d_array(place_1, mino_pos.x + picked_mino.cell[1].x, mino_pos.y + picked_mino.cell[1].y, picked_mino.cell[1].type);
42 | return replace_2d_array(place_2, mino_pos.x + picked_mino.cell[2].x, mino_pos.y + picked_mino.cell[2].y, picked_mino.cell[2].type);
43 | }
44 | else {
45 | return structuredClone(prev_data[0]);
46 | }
47 | })();
48 | const new_laser = [
49 | simulate_laser(new_board, prev_data[2][0].start),
50 | simulate_laser(new_board, prev_data[2][1].start)
51 | ];
52 |
53 | return [
54 | new_board,
55 | [
56 | ...prev_data[1].slice(0, index),
57 | {
58 | ...prev_data[1][index],
59 | pos: placeable
60 | ? mino_pos
61 | : undefined
62 | },
63 | ...prev_data[1].slice(index + 1)
64 | ],
65 | [
66 | { ...prev_data[2][0], board: new_laser[0][0], vertex: new_laser[0][4] },
67 | { ...prev_data[2][1], board: new_laser[1][0], vertex: new_laser[1][4] }
68 | ]
69 | ]
70 | });
71 | if (update_pos) {
72 | e.target.position(update_pos);
73 | }
74 | if (update_scale) {
75 | e.target.scale(update_scale);
76 | }
77 | }, [index, setPuzzleData, setDraggingMinoIndex, update_pos, update_scale]
78 | );
79 | }
80 |
81 | export default useDropMino;
--------------------------------------------------------------------------------
/src/components/Canvas.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Stage, Layer, Group } from 'react-konva';
3 | import { PuzzleData } from '../puzzle/const';
4 | import Laser from './Laser';
5 | import BoardMino from './BoardMino';
6 | import Board from './Board';
7 | import InventoryMino from './InventoryMino';
8 | import OverlayMino from './OverlayMino';
9 | import StartPoint from "./StartPoint";
10 | import EndPoint from "./EndPoint";
11 | import PortraitInventory from './PortraitInventory';
12 | import LandscapeInventory from './LandscapeInventory';
13 |
14 | type CanvasProp = {
15 | width: number,
16 | height: number,
17 | puzzle_data: PuzzleData,
18 | setPuzzleData: React.Dispatch>,
19 | setSolved: React.Dispatch>,
20 | timer_enabled: boolean
21 | };
22 | const Canvas = ({ width, height, puzzle_data, setPuzzleData, setSolved, timer_enabled }: CanvasProp) => {
23 | const [dragging_mino_index, setDraggingMinoIndex] = useState(undefined);
24 |
25 | return (
26 |
30 |
33 |
38 |
39 |
40 |
41 |
42 |
43 | }
44 | />
45 | height}
49 | />
50 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | height}
71 | >
72 |
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
90 |
91 | );
92 | }
93 |
94 |
95 | export default React.memo(Canvas);
96 | // export default Canvas;
97 |
--------------------------------------------------------------------------------
/src/puzzle/const.ts:
--------------------------------------------------------------------------------
1 | export const empty_board = [
2 | ["#", "#", "#", "#", "#", "#", "#"],
3 | ["#", " ", " ", " ", " ", " ", "#"],
4 | ["#", " ", " ", " ", " ", " ", "#"],
5 | ["#", " ", " ", " ", " ", " ", "#"],
6 | ["#", " ", " ", " ", " ", " ", "#"],
7 | ["#", " ", " ", " ", " ", " ", "#"],
8 | ["#", "#", "#", "#", "#", "#", "#"]
9 | ];
10 |
11 | type MinoPattern = {
12 | protrusion: { x: number, y: number }[],
13 | offset: { x: number, y: number },
14 | vertex: number[]
15 | }
16 |
17 | export const mino_pattern: MinoPattern[] = [
18 | { protrusion: [{ x: 0, y: -2 }, { x: 0, y: -1 }], offset: { x: 0, y: 1 }, vertex: [0, -50, 50, -50, 50, 100, 0, 100] },
19 | { protrusion: [{ x: 0, y: -1 }, { x: 0, y: 1 }], offset: { x: 0, y: 0 }, vertex: [0, -50, 50, -50, 50, 100, 0, 100] },
20 | { protrusion: [{ x: 0, y: 1 }, { x: 0, y: 2 }], offset: { x: 0, y: -1 }, vertex: [0, -50, 50, -50, 50, 100, 0, 100] },
21 | { protrusion: [{ x: -2, y: 0 }, { x: -1, y: 0 }], offset: { x: 1, y: 0 }, vertex: [-50, 0, 100, 0, 100, 50, -50, 50] },
22 | { protrusion: [{ x: -1, y: 0 }, { x: 1, y: 0 }], offset: { x: 0, y: 0 }, vertex: [-50, 0, 100, 0, 100, 50, -50, 50] },
23 | { protrusion: [{ x: 1, y: 0 }, { x: 2, y: 0 }], offset: { x: -1, y: 0 }, vertex: [-50, 0, 100, 0, 100, 50, -50, 50] },
24 | { protrusion: [{ x: 0, y: -1 }, { x: -1, y: 0 }], offset: { x: 0, y: 0 }, vertex: [0, 0, 0, -50, 50, -50, 50, 50, -50, 50, -50, 0] },
25 | { protrusion: [{ x: 0, y: -1 }, { x: 1, y: 0 }], offset: { x: 0, y: 0 }, vertex: [0, 50, 0, -50, 50, -50, 50, 0, 100, 0, 100, 50] },
26 | { protrusion: [{ x: 1, y: 0 }, { x: 0, y: 1 }], offset: { x: 0, y: 0 }, vertex: [0, 100, 0, 0, 100, 0, 100, 50, 50, 50, 50, 100] },
27 | { protrusion: [{ x: -1, y: 0 }, { x: 0, y: 1 }], offset: { x: 0, y: 0 }, vertex: [-50, 0, 50, 0, 50, 100, 0, 100, 0, 50, -50, 50] },
28 | { protrusion: [{ x: -1, y: 1 }, { x: 0, y: 1 }], offset: { x: 0, y: -1 }, vertex: [0, 0, 0, -50, 50, -50, 50, 50, -50, 50, -50, 0] },
29 | { protrusion: [{ x: -1, y: -1 }, { x: -1, y: 0 }], offset: { x: 1, y: 0 }, vertex: [0, 50, 0, -50, 50, -50, 50, 0, 100, 0, 100, 50] },
30 | { protrusion: [{ x: 0, y: -1 }, { x: 1, y: -1 }], offset: { x: 0, y: 1 }, vertex: [0, 100, 0, 0, 100, 0, 100, 50, 50, 50, 50, 100] },
31 | { protrusion: [{ x: 1, y: 0 }, { x: 1, y: 1 }], offset: { x: -1, y: 0 }, vertex: [-50, 0, 50, 0, 50, 100, 0, 100, 0, 50, -50, 50] },
32 | { protrusion: [{ x: 0, y: 1 }, { x: 1, y: 1 }], offset: { x: 0, y: -1 }, vertex: [0, 50, 0, -50, 50, -50, 50, 0, 100, 0, 100, 50] },
33 | { protrusion: [{ x: -1, y: 0 }, { x: -1, y: 1 }], offset: { x: 1, y: 0 }, vertex: [0, 100, 0, 0, 100, 0, 100, 50, 50, 50, 50, 100] },
34 | { protrusion: [{ x: -1, y: -1 }, { x: 0, y: -1 }], offset: { x: 0, y: 1 }, vertex: [-50, 0, 50, 0, 50, 100, 0, 100, 0, 50, -50, 50] },
35 | { protrusion: [{ x: 1, y: -1 }, { x: 1, y: 0 }], offset: { x: -1, y: 0 }, vertex: [0, 0, 0, -50, 50, -50, 50, 50, -50, 50, -50, 0] },
36 | ];
37 |
38 | export type MinoData = {
39 | cell: { x: number, y: number, type: string }[],
40 | vertex: number[],
41 | pos: { x: number, y: number } | undefined
42 | };
43 |
44 | export type LaserData = {
45 | start: { x: number; y: number },
46 | end: { x: number; y: number },
47 | board: string[][],
48 | vertex: number[]
49 | };
50 |
51 | export type PuzzleData = [
52 | board: string[][],
53 | mino_data: MinoData[],
54 | laser: LaserData[]
55 | ];
56 |
57 | export const empty_puzzle_data: PuzzleData = [
58 | empty_board,
59 | [
60 | { cell: [{ x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }], vertex: [], pos: undefined },
61 | { cell: [{ x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }], vertex: [], pos: undefined },
62 | { cell: [{ x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }], vertex: [], pos: undefined },
63 | { cell: [{ x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }, { x: 0, y: 0, type: "■" }], vertex: [], pos: undefined },
64 | ],
65 | [
66 | { start: { "x": 3, "y": 3 }, end: { "x": 3, "y": 3 }, board: empty_board, vertex: [] },
67 | { start: { "x": 3, "y": 3 }, end: { "x": 3, "y": 3 }, board: empty_board, vertex: [] }
68 | ]
69 | ];
70 |
71 | export const decode_table: { cell: [string, { x: number, y: number, type: string }][], vertex: [string, number[]][], pos: [string, { x: number, y: number }][] } = {
72 | cell: [
73 | ["A", { "x": 0, "y": -1, "type": "■" }],
74 | ["B", { "x": -1, "y": 0, "type": "■" }],
75 | ["C", { "x": 0, "y": 0, "type": "■" }],
76 | ["D", { "x": 1, "y": 0, "type": "■" }],
77 | ["E", { "x": 0, "y": 1, "type": "■" }],
78 |
79 | ["F", { "x": 0, "y": -1, "type": "/" }],
80 | ["G", { "x": -1, "y": 0, "type": "/" }],
81 | ["H", { "x": 0, "y": 0, "type": "/" }],
82 | ["I", { "x": 1, "y": 0, "type": "/" }],
83 | ["J", { "x": 0, "y": 1, "type": "/" }],
84 |
85 | ["K", { "x": 0, "y": -1, "type": "\\" }],
86 | ["L", { "x": -1, "y": 0, "type": "\\" }],
87 | ["M", { "x": 0, "y": 0, "type": "\\" }],
88 | ["N", { "x": 1, "y": 0, "type": "\\" }],
89 | ["O", { "x": 0, "y": 1, "type": "\\" }]
90 | ],
91 | vertex: [
92 | ["P", [0, -50, 50, -50, 50, 100, 0, 100]],
93 | ["Q", [-50, 0, 100, 0, 100, 50, -50, 50]],
94 | ["R", [0, 0, 0, -50, 50, -50, 50, 50, -50, 50, -50, 0]],
95 | ["S", [0, 50, 0, -50, 50, -50, 50, 0, 100, 0, 100, 50]],
96 | ["T", [0, 100, 0, 0, 100, 0, 100, 50, 50, 50, 50, 100]],
97 | ["U", [-50, 0, 50, 0, 50, 100, 0, 100, 0, 50, -50, 50]]
98 | ],
99 | pos: [
100 | ["a", { x: 1, y: 0 }],
101 | ["b", { x: 2, y: 0 }],
102 | ["c", { x: 3, y: 0 }],
103 | ["d", { x: 4, y: 0 }],
104 | ["e", { x: 5, y: 0 }],
105 | ["f", { x: 0, y: 1 }],
106 | ["g", { x: 6, y: 1 }],
107 | ["h", { x: 0, y: 2 }],
108 | ["i", { x: 6, y: 2 }],
109 | ["j", { x: 0, y: 3 }],
110 | ["k", { x: 6, y: 3 }],
111 | ["l", { x: 0, y: 4 }],
112 | ["m", { x: 6, y: 4 }],
113 | ["n", { x: 0, y: 5 }],
114 | ["o", { x: 6, y: 5 }],
115 | ["p", { x: 1, y: 6 }],
116 | ["q", { x: 2, y: 6 }],
117 | ["r", { x: 3, y: 6 }],
118 | ["s", { x: 4, y: 6 }],
119 | ["t", { x: 5, y: 6 }]
120 | ]
121 | }
122 |
--------------------------------------------------------------------------------
/src/puzzle/generate.ts:
--------------------------------------------------------------------------------
1 | import { compose_n, while_f, replace_2d_array } from "../utils/function";
2 | import { random } from "../utils/random"
3 | import { empty_board, mino_pattern } from "./const";
4 | import { MinoData, PuzzleData } from "./const";
5 | import { simulate_laser } from "./simulate_laser";
6 |
7 | export function generate(seed: number): PuzzleData {
8 | const rnd = new random(seed);
9 | const insert = (base: string, index: number, other: string) => base.slice(0, index) + other + base.slice(index);
10 | const random_insert = (base: string) => insert(base, rnd.next_int(0, base.length + 2), "S");
11 | const flame = compose_n(2, random_insert)("##################").split("");
12 |
13 | type Move = [0, 1] | [0, -1] | [1, 0] | [-1, 0];
14 | const get_s = (index: number): { x: number, y: number, move: Move } => {
15 | if (index < 5) { return { x: index + 1, y: 0, move: [0, 1] }; }
16 | else if (index < 15) { return { x: (index + 1) % 2 * 6, y: (index - 1 - (index - 1) % 2) / 2 - 1, move: [(index % 2 === 0 ? -1 : 1), 0] }; }
17 | else { return { x: index - 14, y: 6, move: [0, -1] }; }
18 | }
19 |
20 | const mirror_random_count = rnd.next_int(3, 7);
21 | const laser = [
22 | Object.assign({ mirror: mirror_random_count }, get_s(flame.indexOf("S"))),
23 | Object.assign({ mirror: 6 - mirror_random_count }, get_s(flame.lastIndexOf("S"))),
24 | ]
25 |
26 | type DrawLaser = [board: string[][], x: number, y: number, move: Move, mirror: number];
27 | // ミラーを必要数置きつつレーザーを描画する関数
28 | const draw_random_laser = (board: string[][], laser: { mirror: number, x: number, y: number, move: Move }) => {
29 | const move_laser = (data: DrawLaser[]) => {
30 | const current = data[data.length - 1];
31 | const board = current[0];
32 | const x = current[1];
33 | const y = current[2];
34 | const move = current[3];
35 | const mirror = current[4];
36 | // 進行方向の軸で取り出す
37 | const pick: [string, string[], string] = move[0] === 0
38 | ? [board[3][x - move[1]], board.map((a) => a[x]), board[3][x + move[1]]]
39 | : [board[y - move[0]][3], board[y], board[y + move[0]][3]];
40 | // ソート
41 | const sort = move[0] + move[1] < 0
42 | ? [...pick[1]].reverse().map(e => e.replace(/u002F/g, "w").replace(/u005C/g, "/").replace(/w/g, "\\"))
43 | : [...pick[1]];
44 | // 後ろをトリミング
45 | const trim_forward = (() => {
46 | if (move[0] === 0) {
47 | // Y軸移動
48 | if (move[1] === 1) { return [...sort].slice(y + 1, sort.length - 1); }
49 | else { return [...sort].slice(sort.length - y, [...sort].length - 1); }
50 | }
51 | else {
52 | // X軸移動
53 | if (move[0] === 1) { return [...sort].slice(x + 1, sort.length - 1); }
54 | else { return [...sort].slice(sort.length - x, [...sort].length - 1); }
55 | }
56 | })();
57 | // 最初に衝突するミラーから後をトリミング
58 | const trim_mirror = (() => {
59 | const trim_r_mirror = trim_forward.includes("/")
60 | ? [...trim_forward].slice(0, trim_forward.indexOf("/") + 1)
61 | : [...trim_forward];
62 | const trim_l_mirror = trim_forward.includes("\\")
63 | ? [...trim_r_mirror].slice(0, trim_forward.indexOf("\\") + 1)
64 | : [...trim_r_mirror];
65 | if (mirror > 0) {
66 | const trim_deadend_mirror = (() => {
67 | const left_is_wall = [...trim_l_mirror][trim_l_mirror.length - 1] === "/" && pick[0] === "#";
68 | const right_is_wall = [...trim_l_mirror][trim_l_mirror.length - 1] === "\\" && pick[2] === "#";
69 | return left_is_wall || right_is_wall
70 | ? [...trim_l_mirror].slice(0, trim_l_mirror.length - 1)
71 | : [...trim_l_mirror];
72 | })();
73 | return trim_deadend_mirror;
74 | }
75 | else {
76 | return [...trim_l_mirror];
77 | }
78 | })();
79 | // 移動可能なマスまでの距離の配列
80 | const range = [...trim_mirror].map((e, index) => e !== "■" ? index : -1).filter(e => e !== -1);
81 | // その中からランダムに決める ミラーを置く必要がないなら最長を選ぶ
82 | const random_range = mirror > 0
83 | ? range[rnd.next_int(0, range.length)]
84 | : range[range.length - 1];
85 | // 1マス進んでboardに書き込む
86 | const draw_laser = (data: DrawLaser) => {
87 | const x = data[1] + data[3][0];
88 | const y = data[2] + data[3][1];
89 | const board = replace_2d_array(data[0], x, y, "■");
90 | const new_data: DrawLaser = [board, x, y, data[3], data[4]];
91 | return new_data;
92 | }
93 | const lined_data: DrawLaser = random_range > 0
94 | ? compose_n(random_range, draw_laser)(current)
95 | : current;
96 | // ミラー設置関数
97 | const set_mirror = (data: DrawLaser) => {
98 | const x = data[1] + data[3][0];
99 | const y = data[2] + data[3][1];
100 | const random_turn = rnd.next_bool();
101 | const mirror = data[0][y][x] === " "
102 | ? data[4] - 1
103 | : data[4];
104 | const board = (() => {
105 | if (data[0][y][x] === " ") {
106 | return random_turn
107 | ? replace_2d_array(data[0], x, y, data[3][0] !== 0 ? "\\" : "/")
108 | : replace_2d_array(data[0], x, y, data[3][0] === 0 ? "\\" : "/");
109 | }
110 | else {
111 | return structuredClone(data[0]);
112 | }
113 | })();
114 | const new_data: DrawLaser = [board, data[1], data[2], data[3], mirror];
115 | return new_data;
116 | }
117 | // 反射関数
118 | const reflection = (data: DrawLaser) => {
119 | const x = data[1] + data[3][0];
120 | const y = data[2] + data[3][1];
121 | const turn_move = (direction: boolean, move: Move): Move => {
122 | if (direction) {
123 | switch (move[1]) {
124 | case 0: return [0, move[0]];
125 | case 1: return [-1, 0];
126 | case -1: return [1, 0];
127 | // [a, b] => [-b, a] 右折
128 | }
129 | }
130 | else {
131 | switch (move[0]) {
132 | case 0: return [move[1], 0];
133 | case 1: return [0, -1];
134 | case -1: return [0, 1];
135 | // [a, b] => [b, -a] 左折
136 | }
137 | }
138 | };
139 | const move: Move = (() => {
140 | if (data[0][y][x] === "/") {
141 | return turn_move(data[3][0] === 0, data[3])
142 | }
143 | else if (data[0][y][x] === "\\") {
144 | return turn_move(data[3][0] !== 0, data[3])
145 | }
146 | else {
147 | return data[3];
148 | }
149 | })();
150 | const new_data: DrawLaser = [data[0], x, y, move, data[4]];
151 | return new_data;
152 | }
153 | // 返すデータを作成
154 | const new_data: DrawLaser[] = (() => {
155 | if (mirror > 0) {
156 | const result = reflection(set_mirror(lined_data));
157 | if (range.length !== 0) {
158 | return structuredClone(data).concat([[...result]])
159 | }
160 | else {
161 | // 行き止まりならUndo 初回で行き止まりならループが終了するデータを返す
162 | return structuredClone(data).length > 1
163 | ? [...data].slice(0, data.length - 1)
164 | : [[empty_board, 0, 0, [0, 1], 0]];
165 | }
166 | }
167 | else {
168 | const result: DrawLaser = (() => {
169 | const data = reflection(lined_data);
170 | const board = data[0][y][x] === " "
171 | ? replace_2d_array(data[0], data[1] - data[3][0], data[2] - data[3][1], "■")
172 | : data[0];
173 | return [board, data[1], data[2], data[3], data[4]];
174 | })();
175 | return structuredClone(data).concat([[...result]]);
176 | }
177 | })();
178 | return new_data;
179 | }
180 | // レーザー必要数ミラーを接地し壁に衝突するまで処理
181 | const initial: [DrawLaser[], number] = [[[board, laser.x, laser.y, laser.move, laser.mirror]], 100];
182 | const new_data = while_f(initial, s => {
183 | const result = move_laser(s[0]);
184 | const current = result[result.length - 1];
185 | const return_data: [DrawLaser[], number] = [result, s[1] - 1];
186 | return [(current[4] > 0 || current[0][current[2]][current[1]] !== "#") && return_data[1] > 0, return_data];
187 | });
188 | // 最新のデータを返す
189 | return new_data[0][new_data[0].length - 1];
190 | }
191 |
192 | // レーザーを2本描画したボードと開始地点、終了地点を返す関数
193 | const draw_two_laser = (): [board: string[][], start: { x: number, y: number }[], end: { x: number, y: number }[]] => {
194 | const draw_one = (): DrawLaser => {
195 | const data = draw_random_laser(empty_board, laser[0]);
196 | if (data[1] !== laser[1].x || data[2] !== laser[1].y) {
197 | return data;
198 | }
199 | else {
200 | return draw_one();
201 | // レーザーの開始地点同士が繋がっていないデータが出るまで再帰
202 | }
203 | };
204 | const draw_1_data = draw_one();
205 | // console.log("==1==");
206 | // console.log(draw_1_data);
207 | const draw_2_data = draw_random_laser(draw_1_data[0], laser[1]);
208 | // console.log("==2==");
209 | // console.log(draw_2_data);
210 | const mirror_count = [...draw_2_data[0]].join().replace(/[^\\/]/g, "").length;
211 | const laser_cell_count = [...draw_2_data[0]].join().replace(/[^\\/■]/g, "").length;
212 | if (laser_cell_count > 11 && mirror_count === 6) {
213 | // if (laser_cell_count > 11) {
214 | const s_drawn_board = (() => {
215 | const draw_1 = replace_2d_array(draw_2_data[0], laser[0].x, laser[0].y, "s");
216 | const draw_2 = replace_2d_array(draw_1, laser[1].x, laser[1].y, "s");
217 | return draw_2;
218 | })();
219 | const all_drawn_board = (() => {
220 | const draw_1 = replace_2d_array(s_drawn_board, draw_1_data[1], draw_1_data[2], "e");
221 | const draw_2 = replace_2d_array(draw_1, draw_2_data[1], draw_2_data[2], "e");
222 | return draw_2;
223 | })();
224 | return [all_drawn_board, [{ x: laser[0].x, y: laser[0].y }, { x: laser[1].x, y: laser[1].y }], [{ x: draw_1_data[1], y: draw_1_data[2] }, { x: draw_2_data[1], y: draw_2_data[2] }]];
225 | }
226 | else {
227 | return draw_two_laser();
228 | // レーザーの通過マスが12以上のデータが出るまで再帰
229 | }
230 | };
231 |
232 | type PlaceMino = [board: string[][], laser_cells: { x: number, y: number }[], mino_data: MinoData[]]
233 | // レーザーが通るマスのランダムな位置にミノを1つ置く関数 置けなかった場合は引数をそのまま返す
234 | const place_random_mino = (data: PlaceMino): PlaceMino => {
235 | const board = data[0];
236 | const random_pos = data[1][rnd.next_int(0, data[1].length)];
237 | const x = random_pos.x;
238 | const y = random_pos.y;
239 | const placeable_cell = [
240 | [" ", " ", board[y - 2]?.[x] ?? "#", " ", " "],
241 | [" ", board[y - 1]?.[x - 1] ?? "#", board[y - 1]?.[x] ?? "#", board[y - 1]?.[x + 1] ?? "#", " "],
242 | [board[y]?.[x - 2] ?? "#", board[y]?.[x - 1] ?? "#", "x", board[y]?.[x + 1] ?? "#", board[y]?.[x + 2] ?? "#"],
243 | [" ", board[y + 1]?.[x - 1] ?? "#", board[y + 1]?.[x] ?? "#", board[y + 1]?.[x + 1] ?? "#", " "],
244 | [" ", " ", board[y + 2]?.[x] ?? "#", " ", " "]
245 | ].map(y => y.map(x => x.replace(/[\\/]/g, "■")));
246 | // console.log([...placeable_cell].join("\n").replace(/,/g, " "));
247 |
248 | // 絶対もっといい方法あるけどとりあえずこれで
249 | const placeable_mino = [
250 | placeable_cell[0][2] === "■" && placeable_cell[1][2] === "■" ? 0 : -1,
251 | placeable_cell[1][2] === "■" && placeable_cell[3][2] === "■" ? 1 : -1,
252 | placeable_cell[3][2] === "■" && placeable_cell[4][2] === "■" ? 2 : -1,
253 | placeable_cell[2][0] === "■" && placeable_cell[2][1] === "■" ? 3 : -1,
254 | placeable_cell[2][1] === "■" && placeable_cell[2][3] === "■" ? 4 : -1,
255 | placeable_cell[2][3] === "■" && placeable_cell[2][4] === "■" ? 5 : -1,
256 | placeable_cell[1][2] === "■" && placeable_cell[2][1] === "■" ? 6 : -1,
257 | placeable_cell[1][2] === "■" && placeable_cell[2][3] === "■" ? 7 : -1,
258 | placeable_cell[2][3] === "■" && placeable_cell[3][2] === "■" ? 8 : -1,
259 | placeable_cell[2][1] === "■" && placeable_cell[3][2] === "■" ? 9 : -1,
260 | placeable_cell[3][1] === "■" && placeable_cell[3][2] === "■" ? 10 : -1,
261 | placeable_cell[1][1] === "■" && placeable_cell[2][1] === "■" ? 11 : -1,
262 | placeable_cell[1][2] === "■" && placeable_cell[1][3] === "■" ? 12 : -1,
263 | placeable_cell[2][3] === "■" && placeable_cell[3][3] === "■" ? 13 : -1,
264 | placeable_cell[3][2] === "■" && placeable_cell[3][3] === "■" ? 14 : -1,
265 | placeable_cell[2][1] === "■" && placeable_cell[3][1] === "■" ? 15 : -1,
266 | placeable_cell[1][1] === "■" && placeable_cell[1][2] === "■" ? 16 : -1,
267 | placeable_cell[1][3] === "■" && placeable_cell[2][3] === "■" ? 17 : -1,
268 | ].filter(e => e !== -1);
269 |
270 | if (placeable_mino.length > 0) {
271 | const random_mino_id = placeable_mino[rnd.next_int(0, placeable_mino.length)];
272 | const place_mino = mino_pattern[random_mino_id];
273 | const place_cell = [
274 | { x: x + place_mino.protrusion[0].x, y: y + place_mino.protrusion[0].y, },
275 | { x: x + place_mino.protrusion[1].x, y: y + place_mino.protrusion[1].y, }
276 | ]
277 | const place_1 = replace_2d_array(board, x, y, `${random_mino_id}`);
278 | const place_2 = replace_2d_array(place_1, place_cell[0].x, place_cell[0].y, `${random_mino_id}`);
279 | const place_3 = replace_2d_array(place_2, place_cell[1].x, place_cell[1].y, `${random_mino_id}`);
280 | const laser_cells = data[1].filter(e => JSON.stringify(e) !== JSON.stringify(random_pos) && JSON.stringify(e) !== JSON.stringify(place_cell[0]) && JSON.stringify(e) !== JSON.stringify(place_cell[1]));
281 | const mino_data: MinoData[] = [
282 | ...data[2],
283 | {
284 | cell: [
285 | {
286 | x: place_mino.offset.x,
287 | y: place_mino.offset.y,
288 | type: board[y][x]
289 | },
290 | {
291 | x: place_mino.protrusion[0].x + place_mino.offset.x,
292 | y: place_mino.protrusion[0].y + place_mino.offset.y,
293 | type: board[y + place_mino.protrusion[0].y][x + place_mino.protrusion[0].x]
294 | },
295 | {
296 | x: place_mino.protrusion[1].x + place_mino.offset.x,
297 | y: place_mino.protrusion[1].y + place_mino.offset.y,
298 | type: board[y + place_mino.protrusion[1].y][x + place_mino.protrusion[1].x]
299 | }
300 | ],
301 | vertex: place_mino.vertex,
302 | pos: undefined
303 | }
304 | ];
305 | return [place_3, laser_cells, mino_data];
306 | }
307 | else {
308 | return data;
309 | }
310 | }
311 |
312 | const initial: [board: string[][], mino_data: MinoData[], start: { x: number, y: number }[], end: { x: number, y: number }[]] = [[[]], [], [], []];
313 | // ボードの二次元配列、ミノのデータ、レーザーの開始地点、終了地点を返す関数
314 | const puzzle_data = while_f(initial, s => {
315 | const laser_drawn_board = draw_two_laser();
316 | const laser_cells: { x: number, y: number }[] = [...laser_drawn_board[0]].map((y, y_index) => y.map((e, x_index) => e === "\\" || e === "/" || e === "■" ? { x: x_index, y: y_index } : { x: -1, y: -1 }).filter(e => e.x !== -1)).flat();
317 | const place_1 = place_random_mino([laser_drawn_board[0], laser_cells, []]);
318 | const place_2 = place_random_mino(place_1);
319 | const place_3 = place_random_mino(place_2);
320 | const place_4 = place_random_mino(place_3);
321 | const return_data: [board: string[][], mino_data: MinoData[], start: { x: number, y: number }[], end: { x: number, y: number }[]] = [laser_drawn_board[0], place_4[2], laser_drawn_board[1], laser_drawn_board[2]];
322 | return [[...place_4[0]].flat().includes("/") || [...place_4[0]].flat().includes("\\") || place_4[2].length !== 4, return_data];
323 | });
324 |
325 | // console.log("==generate==");
326 | // console.log(puzzle_data[0].map(y => y.map(e => e.length === 1 ? ` ${e}` : e)).join("\n").replace(/,/g, " "));
327 |
328 | const laser_board = [
329 | simulate_laser(empty_board, { x: laser[0].x, y: laser[0].y }),
330 | simulate_laser(empty_board, { x: laser[1].x, y: laser[1].y })
331 | ]
332 | return [
333 | [...puzzle_data[0]].map(y => y.map(e => (e !== "#" && e !== "s" && e !== "e") ? " " : e)),
334 | puzzle_data[1],
335 | [
336 | {
337 | start: puzzle_data[2][0],
338 | end: puzzle_data[3][0],
339 | board: laser_board[0][0],
340 | vertex: laser_board[0][4]
341 | },
342 | {
343 | start: puzzle_data[2][1],
344 | end: puzzle_data[3][1],
345 | board: laser_board[1][0],
346 | vertex: laser_board[1][4]
347 | }
348 | ]
349 | ];
350 | }
351 |
352 |
--------------------------------------------------------------------------------
/src/components/ReflecMino.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import { ThemeProvider } from '@mui/material/styles';
3 | import CssBaseline from '@mui/material/CssBaseline';
4 | import { Box, Button, Divider, Fab, IconButton, Link, Paper, Snackbar, Typography } from '@mui/material';
5 | import Grid from '@mui/material/Unstable_Grid2';
6 | import { gh_dark as theme } from '../theme/gh_dark';
7 | import { generate } from '../puzzle/generate';
8 | import Measure from 'react-measure'
9 | import { PuzzleData, empty_puzzle_data } from '../puzzle/const';
10 | import Canvas from './Canvas';
11 | import icon_img from '../images/icon.png';
12 | import h2p1_img from '../images/how_to_play_1.gif';
13 | import h2p2_img from '../images/how_to_play_2.gif';
14 | import h2p3_img from '../images/how_to_play_3.png';
15 | import Timer from './Timer';
16 | import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
17 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'
18 | import { format, isAfter, isBefore } from 'date-fns';
19 | import PauseIcon from '@mui/icons-material/Pause'
20 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'
21 | import ShareIcon from '@mui/icons-material/Share'
22 | import HomeIcon from '@mui/icons-material/Home'
23 | import DoneIcon from '@mui/icons-material/Done'
24 | import RandomDateIcon from '@mui/icons-material/History'
25 | import parse from 'date-fns/parse'
26 | import { is_invalid_date } from '../utils/function';
27 | import { decode } from '../puzzle/decode';
28 |
29 | const query_params = Object.fromEntries(window.location.search.slice(1).split('&').map(e => e.split("=")));
30 | const initial_date = (() => {
31 | const parse_date = parse(query_params.date, "yyyyMMdd", new Date());
32 | return is_invalid_date(parse_date)
33 | ? new Date()
34 | : parse_date;
35 | })();
36 | const custom_puzzle_data = decode(query_params.custom);
37 |
38 | const ReflecMino = (): JSX.Element => {
39 |
40 | const [date, setDate] = useState(initial_date);
41 | const HandleDateChange = useCallback(
42 | (value: Date | null) => {
43 | if (value !== null) {
44 | if (!is_invalid_date(value)) {
45 | const new_date = isBefore(value, new Date()) ? value : new Date();
46 | console.log(format(new_date, "yyyyMMdd"));
47 | setDate(new_date);
48 | }
49 | }
50 | }, []
51 | );
52 | const HandleRandomDate = useCallback(
53 | () => {
54 | const start_date = new Date("1900-01-01");
55 | const end_date = new Date();
56 | const time_diff = end_date.getTime() - start_date.getTime();
57 | const random_time = Math.random() * time_diff;
58 | const random_date = new Date(start_date.getTime() + random_time);
59 | setDate(random_date);
60 | }, []
61 | );
62 | const [puzzle_data, setPuzzleData] = useState(empty_puzzle_data);
63 | const [solved, setSolved] = useState(false);
64 |
65 | const [size, setSize] = useState<{ x: number, y: number }>({ x: 100, y: 100 });
66 | const onResize = useCallback(
67 | ({ bounds }: { bounds?: { width?: number, height?: number } }) => {
68 | setSize({
69 | x: bounds?.width ?? 0,
70 | y: bounds?.height ?? 0
71 | });
72 | }, []
73 | );
74 |
75 | const [copied_snackbar_visible, setCopiedSnackbarVisible] = useState(false);
76 | const copy_result_to_clipboard = useCallback(
77 | () => {
78 | const text = [
79 | `⬛🟧⬛ ReflecMino ${custom_puzzle_data ? "Custom" : format(date, "yyyy/MM/dd")}`,
80 | `🟧⬜🟦 https://yavu.github.io/yv_reflecmino/`,
81 | `⬛🟦⬛ Solved in ${document.getElementById("timer")?.textContent}`,
82 | ].join("\n");
83 | navigator.clipboard.writeText(text)
84 | .then(function () {
85 | setCopiedSnackbarVisible(true);
86 | window.setTimeout(() => {
87 | setCopiedSnackbarVisible(false);
88 | }, 2000);
89 | console.log("Async: Copying to clipboard was successful");
90 | }, function (err) {
91 | console.error("Async: Could not copy text: ", err);
92 | });
93 | }, [date]
94 | );
95 |
96 | const reload_page = useCallback(
97 | () => {
98 | const url = new URL(window.location.href);
99 | window.history.replaceState("", "", url.pathname);
100 | window.location.reload();
101 | }, []
102 | );
103 | const return_to_top = useCallback(
104 | () => {
105 | if (size.x > size.y) {
106 |
107 | setPlaying(false);
108 | window.setTimeout(() => {
109 | setPuzzleData(empty_puzzle_data);
110 | setSolved(false);
111 | setTimerEnabled(false);
112 | }, 600);
113 | }
114 | else {
115 | reload_page();
116 | }
117 | }, [size, reload_page]
118 | )
119 |
120 | const [playing, setPlaying] = useState(false);
121 | const [timer_enabled, setTimerEnabled] = useState(false);
122 | const game_start = useCallback(
123 | () => {
124 | setPuzzleData(
125 | custom_puzzle_data
126 | ? custom_puzzle_data
127 | : generate(Number(format(date, "yyyyMMdd")))
128 | );
129 | setPlaying(true);
130 | setTimerEnabled(true);
131 | }, [date]
132 | );
133 |
134 | const [how2play_visible, setHow2PlayVisible] = useState(false);
135 | const toggle_how2play = useCallback(
136 | () => {
137 | setHow2PlayVisible(!how2play_visible);
138 | }, [how2play_visible]
139 | );
140 |
141 | return (
142 | <>
143 |
149 |
150 |
151 |
152 |
161 |
169 | ReflecMino
170 |
171 |
177 | < Grid
178 | container
179 | direction={"column"}
180 | flex-wrap={"nowrap"}
181 | justifyContent={"flex-start"}
182 | alignItems={"flex-end"}
183 | alignContent={"center"}
184 | >
185 |
206 | Pause
207 |
208 |
231 |
256 | < Grid
257 | container
258 | direction={"column"}
259 | justifyContent={"flex-start"}
260 | alignItems={"center"}
261 | >
262 |
268 | setTimerEnabled(!timer_enabled), [timer_enabled])}
283 | >
284 |
292 |
300 |
301 |
314 | Solved
315 |
316 |
320 | {custom_puzzle_data ? "Custom" : format(date, "yyyy/MM/dd")}
321 |
322 |
329 |
330 |
331 |
347 |
348 | {({ measureRef }) => (
349 |
355 |
362 |
370 |
371 | )}
372 |
373 |
374 |
393 | < Grid
394 | container
395 | direction={"column"}
396 | justifyContent={"flex-start"}
397 | alignItems={"center"}
398 | >
399 |
403 | HowToPlay
404 |
405 |
409 | すべてのタイルを光らせましょう
410 |
411 | < Grid
412 | container
413 | direction={"row"}
414 | flexWrap={"nowrap"}
415 | justifyContent={"flex-start"}
416 | alignItems={"flex-start"}
417 | width={theme.spacing(20)}
418 | height={theme.spacing(16)}
419 | marginTop={theme.spacing(0.5)}
420 | sx={{
421 | overflowX: "scroll",
422 | overflowY: "hidden",
423 | "@media screen and (max-width:704px)": {
424 | width: theme.spacing(20),
425 | }
426 | }}
427 | >
428 |
432 | 1.
433 |
434 |
439 |
443 | 2.
444 |
445 |
450 |
454 | 3.
455 |
456 |
459 |
467 |
468 |
476 |
477 |
494 |
495 |
496 |
515 | < Grid
516 | container
517 | direction={"column"}
518 | justifyContent={"flex-start"}
519 | alignItems={"center"}
520 | >
521 |
530 |
534 | ReflecMino
535 |
536 |
541 | Custom
542 |
543 |
544 | < Grid
545 | container
546 | direction={"row"}
547 | justifyContent={"center"}
548 | alignItems={"center"}
549 | width={theme.spacing(14)}
550 | marginTop={theme.spacing(2)}
551 | display={custom_puzzle_data ? "none" : "inline-flex"}
552 | >
553 |
561 |
562 |
563 |
584 |
585 |
586 |
602 |
619 |
620 |
621 |
638 | < Grid
639 | container
640 | flexDirection={"row"}
641 | flexWrap={"nowrap"}
642 | justifyContent={"space-between"}
643 | alignItems={"center"}
644 | alignContent={"center"}
645 | width={"100%"}
646 | height={"100%"}
647 | sx={{
648 | "@media screen and (max-width:704px)": {
649 | flexDirection: "column",
650 | gap: theme.spacing(1)
651 | }
652 | }}
653 | >
654 | }
671 | >
672 | Share
673 |
674 | }
691 | >
692 | Top
693 |
694 |
699 |
700 |
701 |
708 |
709 |
710 |
715 | Copyright © yavu All Rights Reserved.
716 |
717 |
718 |
719 | >
720 | )
721 | }
722 |
723 | export default React.memo(ReflecMino);
724 | // export default App;
--------------------------------------------------------------------------------