├── 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 |
144 | 148 |
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 | {"move"} 439 | 443 | 2. 444 | 445 | {"move"} 450 | 454 | 3. 455 | 456 | 459 | 467 | 468 | {"move"} 476 | 477 | 494 | 495 | 496 | 515 | < Grid 516 | container 517 | direction={"column"} 518 | justifyContent={"flex-start"} 519 | alignItems={"center"} 520 | > 521 | {"icon"} 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 | 674 | 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; --------------------------------------------------------------------------------