Mostly functional implementation using redux
This commit is contained in:
@@ -6,8 +6,9 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = false
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
quote=singleQuote
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|||||||
483
.eslintrc.js
483
.eslintrc.js
@@ -1,241 +1,264 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
env: {
|
env: {
|
||||||
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
node: true,
|
|
||||||
react: true,
|
|
||||||
},
|
},
|
||||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
extends: ['plugin:react/recommended', 'eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
ecmaVersion: 12,
|
ecmaVersion: 12,
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['react', '@typescript-eslint'],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: ["@typescript-eslint"],
|
|
||||||
rules: {
|
rules: {
|
||||||
"accessor-pairs": "error",
|
// note you must disable the base rule as it can report incorrect errors
|
||||||
"array-bracket-newline": "error",
|
'no-use-before-define': 'off',
|
||||||
"array-bracket-spacing": "error",
|
'@typescript-eslint/no-use-before-define': ['error'],
|
||||||
"array-callback-return": "error",
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
"array-element-newline": 0,
|
'react/prop-types': 0,
|
||||||
"arrow-body-style": "error",
|
'accessor-pairs': 'error',
|
||||||
"arrow-parens": "error",
|
'array-bracket-newline': 'error',
|
||||||
"arrow-spacing": "error",
|
'array-bracket-spacing': 'error',
|
||||||
"block-scoped-var": "error",
|
'array-callback-return': 'error',
|
||||||
"block-spacing": "error",
|
'array-element-newline': 0,
|
||||||
"brace-style": "error",
|
'arrow-body-style': 'error',
|
||||||
"callback-return": "error",
|
'arrow-parens': 0,
|
||||||
|
'arrow-spacing': 'error',
|
||||||
|
'block-scoped-var': 'error',
|
||||||
|
'block-spacing': 'error',
|
||||||
|
'brace-style': 'error',
|
||||||
|
'callback-return': 'error',
|
||||||
camelcase: 0,
|
camelcase: 0,
|
||||||
"capitalized-comments": 0,
|
'capitalized-comments': 0,
|
||||||
"class-methods-use-this": "error",
|
'class-methods-use-this': 'error',
|
||||||
"comma-dangle": 0,
|
'comma-dangle': 0,
|
||||||
"comma-spacing": "error",
|
'comma-spacing': 'error',
|
||||||
"comma-style": "error",
|
'comma-style': 'error',
|
||||||
complexity: "error",
|
complexity: 'error',
|
||||||
"computed-property-spacing": "error",
|
'computed-property-spacing': 'error',
|
||||||
"consistent-return": "error",
|
'consistent-return': 'error',
|
||||||
"consistent-this": "error",
|
'consistent-this': 'error',
|
||||||
curly: "error",
|
curly: 'error',
|
||||||
"default-case": "error",
|
'default-case': 'error',
|
||||||
"default-case-last": "error",
|
'default-case-last': 'error',
|
||||||
"default-param-last": "error",
|
'default-param-last': 'error',
|
||||||
"dot-location": "error",
|
'dot-location': 0,
|
||||||
"dot-notation": "error",
|
'dot-notation': 'error',
|
||||||
"eol-last": "error",
|
'eol-last': 'error',
|
||||||
eqeqeq: "error",
|
eqeqeq: 'error',
|
||||||
"func-call-spacing": "error",
|
'func-call-spacing': 'error',
|
||||||
"func-name-matching": "error",
|
'func-name-matching': 'error',
|
||||||
"func-names": "error",
|
'func-names': 'error',
|
||||||
"func-style": "error",
|
'func-style': 'error',
|
||||||
"function-call-argument-newline": 0,
|
'function-call-argument-newline': 0,
|
||||||
"function-paren-newline": "error",
|
'function-paren-newline': 0,
|
||||||
"generator-star-spacing": "error",
|
'generator-star-spacing': 0,
|
||||||
"global-require": "error",
|
'global-require': 'error',
|
||||||
"grouped-accessor-pairs": "error",
|
'grouped-accessor-pairs': 'error',
|
||||||
"guard-for-in": "error",
|
'guard-for-in': 'error',
|
||||||
"handle-callback-err": "error",
|
'handle-callback-err': 'error',
|
||||||
"id-blacklist": "error",
|
'id-blacklist': 'error',
|
||||||
"id-denylist": "error",
|
'id-denylist': 'error',
|
||||||
"id-length": "error",
|
'id-length': 'error',
|
||||||
"id-match": "error",
|
'id-match': 'error',
|
||||||
"implicit-arrow-linebreak": "error",
|
'implicit-arrow-linebreak': 0,
|
||||||
indent: ["error", 2],
|
indent: 0,
|
||||||
"indent-legacy": ["error", 2],
|
// indent: [
|
||||||
"init-declarations": "error",
|
// 'error',
|
||||||
"jsx-quotes": "error",
|
// 2,
|
||||||
"key-spacing": "error",
|
// {
|
||||||
"keyword-spacing": "error",
|
// SwitchCase: 1,
|
||||||
"line-comment-position": "error",
|
// MemberExpression: 1,
|
||||||
"linebreak-style": "error",
|
// CallExpression: { arguments: 1 },
|
||||||
"lines-around-comment": "error",
|
// offsetTernaryExpressions: true,
|
||||||
"lines-around-directive": "error",
|
// flatTernaryExpressions: true,
|
||||||
"lines-between-class-members": "error",
|
// ignoredNodes: ['ConditionalExpression'],
|
||||||
"max-classes-per-file": "error",
|
// },
|
||||||
"max-depth": "error",
|
// ],
|
||||||
"max-len": ["error", 180],
|
'indent-legacy': 0,
|
||||||
"max-lines": "error",
|
'init-declarations': 'error',
|
||||||
"max-lines-per-function": "error",
|
'jsx-quotes': 'error',
|
||||||
"max-nested-callbacks": "error",
|
'key-spacing': 'error',
|
||||||
"max-params": "error",
|
'keyword-spacing': 'error',
|
||||||
"max-statements": 0,
|
'line-comment-position': 'error',
|
||||||
"max-statements-per-line": "error",
|
'linebreak-style': 'error',
|
||||||
"multiline-comment-style": 0,
|
'lines-around-comment': 0,
|
||||||
"multiline-ternary": "error",
|
'lines-around-directive': 'error',
|
||||||
"new-cap": 0,
|
'lines-between-class-members': 'error',
|
||||||
"new-parens": "error",
|
'max-classes-per-file': 'error',
|
||||||
"newline-after-var": 0,
|
'max-depth': 'error',
|
||||||
"newline-before-return": 0,
|
'max-len': ['error', 180],
|
||||||
"newline-per-chained-call": 0,
|
'max-lines': 'error',
|
||||||
"no-alert": "error",
|
'max-lines-per-function': 0,
|
||||||
"no-array-constructor": "error",
|
'max-nested-callbacks': 'error',
|
||||||
"no-await-in-loop": "error",
|
'max-params': 0,
|
||||||
"no-bitwise": "error",
|
'max-statements': 0,
|
||||||
"no-buffer-constructor": "error",
|
'max-statements-per-line': 'error',
|
||||||
"no-caller": "error",
|
'multiline-comment-style': 0,
|
||||||
"no-catch-shadow": "error",
|
'multiline-ternary': 0,
|
||||||
"no-confusing-arrow": "error",
|
'new-cap': 0,
|
||||||
"no-console": "warn",
|
'new-parens': 'error',
|
||||||
"no-constructor-return": "error",
|
'newline-after-var': 0,
|
||||||
"no-continue": "error",
|
'newline-before-return': 0,
|
||||||
"no-div-regex": "error",
|
'newline-per-chained-call': 0,
|
||||||
"no-duplicate-imports": "error",
|
'no-alert': 'error',
|
||||||
"no-else-return": "error",
|
'no-array-constructor': 'error',
|
||||||
"no-empty-function": "error",
|
'no-await-in-loop': 'error',
|
||||||
"no-eq-null": "error",
|
'no-bitwise': 'error',
|
||||||
"no-eval": "error",
|
'no-buffer-constructor': 'error',
|
||||||
"no-extend-native": "error",
|
'no-caller': 'error',
|
||||||
"no-extra-bind": "error",
|
'no-catch-shadow': 'error',
|
||||||
"no-extra-label": "error",
|
'no-confusing-arrow': 0,
|
||||||
"no-extra-parens": "error",
|
'no-console': 'warn',
|
||||||
"no-floating-decimal": "error",
|
'no-constructor-return': 'error',
|
||||||
"no-implicit-coercion": "error",
|
'no-continue': 0,
|
||||||
"no-implicit-globals": "error",
|
'no-div-regex': 'error',
|
||||||
"no-implied-eval": "error",
|
'no-duplicate-imports': 'error',
|
||||||
"no-inline-comments": "error",
|
'no-else-return': 'error',
|
||||||
"no-invalid-this": "error",
|
'no-empty-function': 'error',
|
||||||
"no-iterator": "error",
|
'no-eq-null': 'error',
|
||||||
"no-label-var": "error",
|
'no-eval': 'error',
|
||||||
"no-labels": "error",
|
'no-extend-native': 'error',
|
||||||
"no-lone-blocks": "error",
|
'no-extra-bind': 'error',
|
||||||
"no-lonely-if": "error",
|
'no-extra-label': 'error',
|
||||||
"no-loop-func": "error",
|
'no-extra-parens': 0,
|
||||||
"no-loss-of-precision": "error",
|
'no-floating-decimal': 'error',
|
||||||
"no-magic-numbers": 0,
|
'no-implicit-coercion': 'error',
|
||||||
"no-mixed-operators": "error",
|
'no-implicit-globals': 'error',
|
||||||
"no-mixed-requires": "error",
|
'no-implied-eval': 'error',
|
||||||
"no-multi-assign": "error",
|
'no-inline-comments': 'error',
|
||||||
"no-multi-spaces": "error",
|
'no-invalid-this': 'error',
|
||||||
"no-multi-str": "error",
|
'no-iterator': 'error',
|
||||||
"no-multiple-empty-lines": "error",
|
'no-label-var': 'error',
|
||||||
"no-native-reassign": "error",
|
'no-labels': 'error',
|
||||||
"no-negated-condition": "error",
|
'no-lone-blocks': 'error',
|
||||||
"no-negated-in-lhs": "error",
|
'no-lonely-if': 'error',
|
||||||
"no-nested-ternary": "error",
|
'no-loop-func': 'error',
|
||||||
"no-new": "error",
|
'no-loss-of-precision': 'error',
|
||||||
"no-new-func": "error",
|
'no-magic-numbers': 0,
|
||||||
"no-new-object": "error",
|
'no-mixed-operators': 0,
|
||||||
"no-new-require": "error",
|
'no-mixed-requires': 'error',
|
||||||
"no-new-wrappers": "error",
|
'no-multi-assign': 'error',
|
||||||
"no-octal-escape": "error",
|
'no-multi-spaces': 'error',
|
||||||
"no-param-reassign": "error",
|
'no-multi-str': 'error',
|
||||||
"no-path-concat": "error",
|
'no-multiple-empty-lines': 'error',
|
||||||
"no-plusplus": "error",
|
'no-native-reassign': 'error',
|
||||||
"no-process-env": "error",
|
'no-negated-condition': 'error',
|
||||||
"no-process-exit": "error",
|
'no-negated-in-lhs': 'error',
|
||||||
"no-promise-executor-return": "error",
|
'no-nested-ternary': 0,
|
||||||
"no-proto": "error",
|
'no-new': 'error',
|
||||||
"no-restricted-exports": "error",
|
'no-new-func': 'error',
|
||||||
"no-restricted-globals": "error",
|
'no-new-object': 'error',
|
||||||
"no-restricted-imports": "error",
|
'no-new-require': 'error',
|
||||||
"no-restricted-modules": "error",
|
'no-new-wrappers': 'error',
|
||||||
"no-restricted-properties": "error",
|
'no-octal-escape': 'error',
|
||||||
"no-restricted-syntax": "error",
|
'no-param-reassign': 'error',
|
||||||
"no-return-assign": "error",
|
'no-path-concat': 'error',
|
||||||
"no-return-await": "error",
|
'no-plusplus': 'error',
|
||||||
"no-script-url": "error",
|
'no-process-env': 'error',
|
||||||
"no-self-compare": "error",
|
'no-process-exit': 'error',
|
||||||
"no-sequences": "error",
|
'no-promise-executor-return': 'error',
|
||||||
"no-shadow": "error",
|
'no-proto': 'error',
|
||||||
"no-spaced-func": "error",
|
'no-restricted-exports': 'error',
|
||||||
"no-sync": "error",
|
'no-restricted-globals': 'error',
|
||||||
"no-tabs": "error",
|
'no-restricted-imports': 'error',
|
||||||
"no-template-curly-in-string": "error",
|
'no-restricted-modules': 'error',
|
||||||
"no-ternary": "error",
|
'no-restricted-properties': 'error',
|
||||||
"no-throw-literal": "error",
|
'no-restricted-syntax': 'error',
|
||||||
"no-trailing-spaces": "error",
|
'no-return-assign': 'error',
|
||||||
"no-undef-init": "error",
|
'no-return-await': 'error',
|
||||||
"no-undefined": 0,
|
'no-script-url': 'error',
|
||||||
"no-underscore-dangle": "error",
|
'no-self-compare': 'error',
|
||||||
"no-unmodified-loop-condition": "error",
|
'no-sequences': 'error',
|
||||||
"no-unneeded-ternary": "error",
|
'no-shadow': 'error',
|
||||||
"no-unreachable-loop": "error",
|
'no-spaced-func': 'error',
|
||||||
"no-unused-expressions": "error",
|
'no-sync': 'error',
|
||||||
"no-use-before-define": "error",
|
'no-tabs': 'error',
|
||||||
"no-useless-backreference": "error",
|
'no-template-curly-in-string': 'error',
|
||||||
"no-useless-call": "error",
|
'no-ternary': 0,
|
||||||
"no-useless-computed-key": "error",
|
'no-throw-literal': 'error',
|
||||||
"no-useless-concat": "error",
|
'no-trailing-spaces': 'error',
|
||||||
"no-useless-constructor": "error",
|
'no-undef-init': 0,
|
||||||
"no-useless-rename": "error",
|
'no-undefined': 0,
|
||||||
"no-useless-return": "error",
|
'no-underscore-dangle': 'error',
|
||||||
"no-var": "error",
|
'no-unmodified-loop-condition': 'error',
|
||||||
"no-void": "error",
|
'no-unneeded-ternary': 'error',
|
||||||
"no-warning-comments": "error",
|
'no-unreachable-loop': 'error',
|
||||||
"no-whitespace-before-property": "error",
|
'no-unused-expressions': 'error',
|
||||||
"nonblock-statement-body-position": "error",
|
'no-useless-backreference': 'error',
|
||||||
"object-curly-newline": "error",
|
'no-useless-call': 'error',
|
||||||
"object-curly-spacing": 0,
|
'no-useless-computed-key': 'error',
|
||||||
"object-property-newline": "error",
|
'no-useless-concat': 'error',
|
||||||
"object-shorthand": "error",
|
'no-useless-constructor': 'error',
|
||||||
"one-var": 0,
|
'no-useless-rename': 'error',
|
||||||
"one-var-declaration-per-line": 0,
|
'no-useless-return': 'error',
|
||||||
"operator-assignment": "error",
|
'no-var': 'error',
|
||||||
"operator-linebreak": "error",
|
'no-void': 'error',
|
||||||
"padded-blocks": 0,
|
'no-warning-comments': 'error',
|
||||||
"padding-line-between-statements": "error",
|
'no-whitespace-before-property': 'error',
|
||||||
"prefer-arrow-callback": "error",
|
'nonblock-statement-body-position': 'error',
|
||||||
"prefer-const": "error",
|
'object-curly-newline': 'error',
|
||||||
"prefer-destructuring": "error",
|
'object-curly-spacing': 0,
|
||||||
"prefer-exponentiation-operator": "error",
|
'object-property-newline': 0,
|
||||||
"prefer-named-capture-group": "error",
|
'object-shorthand': 'error',
|
||||||
"prefer-numeric-literals": "error",
|
'one-var': 0,
|
||||||
"prefer-object-spread": "error",
|
'one-var-declaration-per-line': 0,
|
||||||
"prefer-promise-reject-errors": "error",
|
'operator-assignment': 'error',
|
||||||
"prefer-reflect": "error",
|
'operator-linebreak': 'error',
|
||||||
"prefer-regex-literals": "error",
|
'padded-blocks': 0,
|
||||||
"prefer-rest-params": "error",
|
'padding-line-between-statements': 'error',
|
||||||
"prefer-spread": "error",
|
'prefer-arrow-callback': 'error',
|
||||||
"prefer-template": "error",
|
'prefer-const': 'error',
|
||||||
"quote-props": ["error", "as-needed"],
|
'prefer-destructuring': 'error',
|
||||||
quotes: ["error", "single"],
|
'prefer-exponentiation-operator': 'error',
|
||||||
radix: "error",
|
'prefer-named-capture-group': 'error',
|
||||||
"require-atomic-updates": "error",
|
'prefer-numeric-literals': 'error',
|
||||||
"require-await": "error",
|
'prefer-object-spread': 'error',
|
||||||
"require-jsdoc": "error",
|
'prefer-promise-reject-errors': 'error',
|
||||||
"require-unicode-regexp": "error",
|
'prefer-reflect': 'error',
|
||||||
"rest-spread-spacing": "error",
|
'prefer-regex-literals': 'error',
|
||||||
semi: "error",
|
'prefer-rest-params': 'error',
|
||||||
"semi-spacing": "error",
|
'prefer-spread': 'error',
|
||||||
"semi-style": "error",
|
'prefer-template': 'error',
|
||||||
"sort-imports": 0,
|
'quote-props': ['error', 'as-needed'],
|
||||||
"sort-keys": 0,
|
quotes: ['error', 'single'],
|
||||||
"sort-vars": 0,
|
radix: 'error',
|
||||||
"space-before-blocks": "error",
|
'require-atomic-updates': 'error',
|
||||||
"space-before-function-paren": "error",
|
'require-await': 'error',
|
||||||
"space-in-parens": "error",
|
'require-jsdoc': 0,
|
||||||
"space-infix-ops": "error",
|
'require-unicode-regexp': 'error',
|
||||||
"space-unary-ops": "error",
|
'rest-spread-spacing': 'error',
|
||||||
"spaced-comment": "error",
|
semi: 'error',
|
||||||
strict: "error",
|
'semi-spacing': 'error',
|
||||||
"switch-colon-spacing": "error",
|
'semi-style': 'error',
|
||||||
"symbol-description": "error",
|
'sort-imports': 0,
|
||||||
"template-curly-spacing": "error",
|
'sort-keys': 0,
|
||||||
"template-tag-spacing": "error",
|
'sort-vars': 0,
|
||||||
"unicode-bom": "error",
|
'space-before-blocks': 'error',
|
||||||
"valid-jsdoc": "error",
|
'space-before-function-paren': 0,
|
||||||
"vars-on-top": "error",
|
'space-in-parens': 'error',
|
||||||
"wrap-iife": "error",
|
'space-infix-ops': 'error',
|
||||||
"wrap-regex": "error",
|
'space-unary-ops': 'error',
|
||||||
"yield-star-spacing": "error",
|
'spaced-comment': 'error',
|
||||||
yoda: "error",
|
strict: 'error',
|
||||||
|
'switch-colon-spacing': 'error',
|
||||||
|
'symbol-description': 'error',
|
||||||
|
'template-curly-spacing': 'error',
|
||||||
|
'template-tag-spacing': 'error',
|
||||||
|
'unicode-bom': 'error',
|
||||||
|
'valid-jsdoc': 'error',
|
||||||
|
'vars-on-top': 'error',
|
||||||
|
'wrap-iife': 'error',
|
||||||
|
'wrap-regex': 'error',
|
||||||
|
'yield-star-spacing': 'error',
|
||||||
|
yoda: 'error',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
|
*bak
|
||||||
|
|||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"proseWrap": "preserve"
|
||||||
|
}
|
||||||
22
package.json
22
package.json
@@ -1,14 +1,25 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "parcel serve src/index.htm"
|
"start": "parcel serve src/index.htm",
|
||||||
|
"type": "tsc --noEmit",
|
||||||
|
"lint": "eslint --ext .ts,.tsx ./src",
|
||||||
|
"lint:fix": "eslint --ext .ts,.tsx ./src --fix",
|
||||||
|
"prettier": "prettier --write 'src/**/*.{ts,tsx,json,md,js,scss}'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^4.0.5",
|
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"@types/styled-components": "^5.1.7",
|
|
||||||
"@types/react-redux": "^7.1.14",
|
"@types/react-redux": "^7.1.14",
|
||||||
"eslint": "^7.13.0"
|
"@types/styled-components": "^5.1.7",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.11.1",
|
||||||
|
"@typescript-eslint/parser": "^4.11.1",
|
||||||
|
"eslint": "^7.17.0",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
|
"eslint-plugin-react": "^7.22.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"typescript": "^4.0.5",
|
||||||
|
"tslib": "^2.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.5.0",
|
"@reduxjs/toolkit": "^1.5.0",
|
||||||
@@ -17,7 +28,6 @@
|
|||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"styled-components": "^5.2.1",
|
"styled-components": "^5.2.1"
|
||||||
"tslib": "^2.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { AppDispatch, AppState, CellValue, Coordinate, dig } from "../store";
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
size: number;
|
|
||||||
board: CellValue[];
|
|
||||||
digs: Record<number, number[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionProps {
|
|
||||||
dig(coordinate: Coordinate): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = ActionProps & StateProps;
|
|
||||||
|
|
||||||
const Cell = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Row = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
${Cell} {
|
|
||||||
border-left: 1px solid silver;
|
|
||||||
border-top: 1px solid silver;
|
|
||||||
|
|
||||||
&:nth-last-child(1) {
|
|
||||||
border-right: 1px solid silver;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:nth-last-child(1) {
|
|
||||||
${Cell} {
|
|
||||||
border-bottom: 1px solid silver;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Board: FC<Props> = ({ dig, digs, board, size }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{[...Array(size).keys()].map((row) => (
|
|
||||||
<Row key={`row-${row}`} id={`row-${row}`}>
|
|
||||||
{[...Array(size).keys()].map((col) => (
|
|
||||||
<Cell
|
|
||||||
id={`row-${row}-col-${col}`}
|
|
||||||
key={`row-${row}-col-${col}`}
|
|
||||||
onClick={() => {
|
|
||||||
dig({ row, col });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/*digs[row]?.includes(col) && board[row * size + col][0] */}
|
|
||||||
{digs[row]?.includes(col)
|
|
||||||
? "D"
|
|
||||||
: board[row * size + col].toString()}
|
|
||||||
</Cell>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({
|
|
||||||
size: state.size,
|
|
||||||
board: state.board,
|
|
||||||
digs: state.digs,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch): ActionProps => ({
|
|
||||||
dig: (coordinate: Coordinate) => dispatch(dig(coordinate)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Board);
|
|
||||||
90
src/components/GameBoard.tsx
Normal file
90
src/components/GameBoard.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { selectBoardRows } from '../selectors';
|
||||||
|
import { AppDispatch, AppState, dig as digAction } from '../store';
|
||||||
|
import { Cell, Coordinate, GameStatus } from '../types';
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
size: number;
|
||||||
|
board: Cell[][];
|
||||||
|
status: GameStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionProps {
|
||||||
|
dig(coordinate: Coordinate): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = ActionProps & StateProps;
|
||||||
|
|
||||||
|
const Cell = styled.div<{ cell: Cell; status: GameStatus }>`
|
||||||
|
display: inline-flex;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid silver;
|
||||||
|
margin: 1px;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
/* Value based style */
|
||||||
|
${({ cell }) => (cell.value === 'B' ? 'color: red;' : cell.value > 0 ? 'color: teal;' : 'color: white;')}
|
||||||
|
|
||||||
|
/* Reveal based style */
|
||||||
|
${({ cell }) =>
|
||||||
|
cell.reveal
|
||||||
|
? `background-color: transparent;
|
||||||
|
cursor: default;`
|
||||||
|
: `background-color: silver;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #838484;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
|
/* Digged based style */
|
||||||
|
${({ cell }) => cell.digged && 'border-color: green;'}
|
||||||
|
|
||||||
|
/* Digged based style */
|
||||||
|
${({ cell }) => !cell.reveal && 'text-indent: -9999px;'} /* ${({ cell }) => !cell.reveal && 'color: rgba(0,0,0,.1)'} */
|
||||||
|
|
||||||
|
${({ status }) =>
|
||||||
|
status !== 'RUNNING' &&
|
||||||
|
`
|
||||||
|
cursor: default;
|
||||||
|
&:hover {
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Row = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GameBoard: FC<Props> = ({ dig, board, status }) => (
|
||||||
|
<>
|
||||||
|
{board.map((row, rowIndex) => (
|
||||||
|
<Row key={`row-${rowIndex}`}>
|
||||||
|
{row.map(cell => (
|
||||||
|
<Cell key={`cell-${cell.row}-${cell.col}`} cell={cell} status={status} onClick={() => dig(cell)}>
|
||||||
|
{cell.value}
|
||||||
|
</Cell>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState): StateProps => ({
|
||||||
|
size: state.size,
|
||||||
|
board: selectBoardRows(state),
|
||||||
|
status: state.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: AppDispatch): ActionProps => ({
|
||||||
|
dig: (coordinate: Coordinate) => dispatch(digAction(coordinate)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(GameBoard);
|
||||||
@@ -1,39 +1,39 @@
|
|||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from 'react';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import { AppDispatch, AppState, startGame } from "../store";
|
import { AppDispatch, AppState, startGame as startGameAction } from '../store';
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
size: number;
|
size: number;
|
||||||
|
numberOfMines: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionProps {
|
interface ActionProps {
|
||||||
startGame(size: number): void;
|
startGame(size: number, mines: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = ActionProps & StateProps;
|
type Props = ActionProps & StateProps;
|
||||||
|
|
||||||
const StartGame: FC<Props> = ({ startGame, size }) => {
|
const StartGame: FC<Props> = ({ startGame, size, numberOfMines }) => {
|
||||||
const [userSize, setUserSize] = useState(size);
|
const [userSize, setUserSize] = useState(size);
|
||||||
|
const [userMines, setUserMines] = useState(numberOfMines);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<label htmlFor="size">Size:</label>
|
<label htmlFor="size">Size:</label>
|
||||||
<input
|
<input type="number" name="size" value={userSize} onChange={ev => setUserSize(parseInt(ev.target.value, 10))} />
|
||||||
type="number"
|
<label htmlFor="size">Mines:</label>
|
||||||
name="size"
|
<input type="number" name="mines" value={userMines} onChange={ev => setUserMines(parseInt(ev.target.value, 10))} />
|
||||||
value={userSize}
|
<button onClick={() => startGame(userSize, userMines)}>Start</button>
|
||||||
onChange={(e) => setUserSize(parseInt(e.target.value, 10))}
|
|
||||||
/>
|
|
||||||
<button onClick={() => startGame(userSize)}>Start</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({
|
const mapStateToProps = (state: AppState): StateProps => ({
|
||||||
size: state.size,
|
size: state.size,
|
||||||
|
numberOfMines: state.numberOfMines,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: AppDispatch): ActionProps => ({
|
const mapDispatchToProps = (dispatch: AppDispatch): ActionProps => ({
|
||||||
startGame: (size: number) => dispatch(startGame(size)),
|
startGame: (size: number, numberOfMines: number) => dispatch(startGameAction({ size, numberOfMines })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(StartGame);
|
export default connect(mapStateToProps, mapDispatchToProps)(StartGame);
|
||||||
|
|||||||
30
src/game.tsx
30
src/game.tsx
@@ -1,27 +1,29 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from 'react';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import Board from "./components/Board";
|
import GameBoard from './components/GameBoard';
|
||||||
import StartGame from "./components/StartGame";
|
import StartGame from './components/StartGame';
|
||||||
import { AppState } from "./store";
|
import { AppState } from './store';
|
||||||
|
import { GameStatus } from './types';
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
started: boolean;
|
started: boolean;
|
||||||
|
status: GameStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = StateProps;
|
type Props = StateProps;
|
||||||
|
|
||||||
const Game: FC<Props> = ({ started }) => {
|
const Game: FC<Props> = ({ started, status }) => (
|
||||||
return (
|
<>
|
||||||
<>
|
<h1>Minesweeper</h1>
|
||||||
<h1>Minesweeper</h1>
|
<h2>{status}</h2>
|
||||||
{!started && <StartGame />}
|
{!started && <StartGame />}
|
||||||
{started && <Board />}
|
{started && <GameBoard />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({
|
const mapStateToProps = (state: AppState): StateProps => ({
|
||||||
started: state.started,
|
started: state.started,
|
||||||
|
status: state.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Game);
|
export default connect(mapStateToProps)(Game);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from 'react-redux';
|
||||||
import { store } from "./store";
|
import { store } from './store';
|
||||||
import Game from "./game";
|
import Game from './Game';
|
||||||
import GlobalStyle from "./styling/GlobalStyle";
|
import GlobalStyle from './styling/GlobalStyle';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<Game />
|
<Game />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
document.getElementById("root")
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|||||||
14
src/selectors.ts
Normal file
14
src/selectors.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { AppState } from './store';
|
||||||
|
import { Cell } from './types';
|
||||||
|
|
||||||
|
export const selectAppState = (state: AppState): AppState => state;
|
||||||
|
|
||||||
|
export const selectBoard = createSelector(selectAppState, (state: AppState): Cell[] => state.board);
|
||||||
|
|
||||||
|
export const selectBoardRows = createSelector(selectBoard, (board: Cell[]): Cell[][] =>
|
||||||
|
board.reduce((rows, cell) => {
|
||||||
|
rows[cell.row] = [...(rows[cell.row] || []), cell];
|
||||||
|
return rows;
|
||||||
|
}, [] as Cell[][])
|
||||||
|
);
|
||||||
217
src/store.ts
217
src/store.ts
@@ -1,98 +1,185 @@
|
|||||||
import { configureStore, createAction, createReducer } from "@reduxjs/toolkit";
|
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { Cell, Coordinate, GameStatus } from './types';
|
||||||
|
import { range, rangeValues, shuffle } from './utils';
|
||||||
|
|
||||||
export type CellValue = "E" | "B" | number;
|
/* ********* TYPES ********************************************************************************** */
|
||||||
export interface Coordinate {
|
|
||||||
row: number;
|
|
||||||
col: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
started: boolean;
|
started: boolean;
|
||||||
|
status: GameStatus;
|
||||||
size: number;
|
size: number;
|
||||||
board: CellValue[];
|
numberOfMines: number;
|
||||||
digs: Record<number, number[]>;
|
board: Cell[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialBoardSize = 10;
|
/* ********* FUNCTIONS ****************************************************************************** */
|
||||||
|
|
||||||
const random = (min: number, max: number): number =>
|
const sameCoordinate = (coordinate1: Coordinate, coordinate2: Coordinate): boolean =>
|
||||||
min + Math.floor(Math.random() * (max - min));
|
coordinate1.row === coordinate2.row && coordinate1.col === coordinate2.col;
|
||||||
|
|
||||||
const createBoard = (size: number, bombs: number = 5): CellValue[] => {
|
const getIndexFromCoordinate = (coordinate: Coordinate, size: number) => coordinate.row * size + coordinate.col;
|
||||||
const board: CellValue[] = Array.from(new Array(size * size), () => "E");
|
|
||||||
|
|
||||||
let bombsLeft = bombs;
|
const neighbouringIndexes = function* neighbouringIndexes(size: number, coordinate: Coordinate): Generator<number> {
|
||||||
while (bombsLeft) {
|
for (const row of range(Math.max(coordinate.row - 1, 0), Math.min(coordinate.row + 2, size))) {
|
||||||
const row = random(0, size);
|
for (const col of range(Math.max(coordinate.col - 1, 0), Math.min(coordinate.col + 2, size))) {
|
||||||
const cell = random(0, size);
|
if (row !== coordinate.row || col !== coordinate.col) {
|
||||||
if (board[row * size + cell] === "B") {
|
yield getIndexFromCoordinate({ row, col }, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const neighbouringCells = function* neighbouringCells(
|
||||||
|
board: Cell[],
|
||||||
|
size: number,
|
||||||
|
coordinate: Coordinate
|
||||||
|
): Generator<Cell> {
|
||||||
|
for (const index of neighbouringIndexes(size, coordinate)) {
|
||||||
|
if (board[index] === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
board[row * size + cell] = "B";
|
yield board[index];
|
||||||
bombsLeft -= 1;
|
|
||||||
}
|
}
|
||||||
board.forEach((cell, index) => {
|
return false;
|
||||||
if (cell === "B") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const row = Math.floor(index / size);
|
|
||||||
const col = index - row * size;
|
|
||||||
let bombCount = 0;
|
|
||||||
for (let y = Math.max(0, row - 1); y <= Math.min(row + 1, size); y += 1) {
|
|
||||||
for (let x = Math.max(0, col - 1); x <= Math.min(col + 1, size); x += 1) {
|
|
||||||
if (board[y * size + x] === "B") {
|
|
||||||
bombCount += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
board[index] = bombCount;
|
|
||||||
});
|
|
||||||
|
|
||||||
return board;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sameCoordinate = (c1: Coordinate, c2: Coordinate): boolean =>
|
const createMines = (size: number, numberOfMines: number): boolean[] =>
|
||||||
c1.col === c2.col && c1.row === c2.row;
|
shuffle([...rangeValues(0, numberOfMines, true), ...rangeValues(0, size ** 2 - numberOfMines, false)]);
|
||||||
|
|
||||||
const dig = (
|
const createBoard = (size: number, numberOfMines: number): Cell[] => {
|
||||||
board: CellValue[],
|
const mines = createMines(size, numberOfMines);
|
||||||
digs: Record<number, number[]>,
|
|
||||||
{ row, col }: Coordinate
|
const countMines = (row: number, col: number): number =>
|
||||||
) => {
|
[...neighbouringIndexes(size, { row, col })].reduce((bombs, index) => bombs + (mines[index] ? 1 : 0), 0);
|
||||||
const size = Math.sqrt(board.length);
|
|
||||||
for (let y = Math.max(0, row - 1); y <= Math.min(row + 1, size); y += 1) {
|
return range(0, size).reduce(
|
||||||
for (let x = Math.max(0, col - 1); x <= Math.min(col + 1, size); x += 1) {
|
(board, row, rowIndex) => [
|
||||||
if (board[y * size + x] === "B") {
|
...board,
|
||||||
|
...range(0, size).map(
|
||||||
|
(col, colIndex) =>
|
||||||
|
({
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
value: mines[rowIndex * size + colIndex] ? 'B' : countMines(row, col),
|
||||||
|
digged: false,
|
||||||
|
reveal: false,
|
||||||
|
} as Cell)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[] as Cell[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allNeighbouringCells = function* allNeighbouringCells(
|
||||||
|
board: Cell[],
|
||||||
|
size: number,
|
||||||
|
coordinate: Coordinate,
|
||||||
|
include: (cell: Cell) => boolean
|
||||||
|
): Generator<Cell> {
|
||||||
|
const visited: number[] = [];
|
||||||
|
const todo: Cell[] = [...neighbouringCells(board, size, coordinate)];
|
||||||
|
let cell: Cell | undefined = undefined;
|
||||||
|
while ((cell = todo.pop())) {
|
||||||
|
const index = getIndexFromCoordinate(cell, size);
|
||||||
|
if (visited.includes(index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.push(index);
|
||||||
|
if (include(cell)) {
|
||||||
|
yield cell;
|
||||||
|
if (cell.value === 0) {
|
||||||
|
todo.push(...neighbouringCells(board, size, cell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ********* DEFAULTS ******************************************************************************* */
|
||||||
|
|
||||||
|
const INITIAL_BOARD_SIZE = 10;
|
||||||
|
const INITIAL_NUMBER_OF_MINES = 8;
|
||||||
|
|
||||||
const defaultAppState: AppState = {
|
const defaultAppState: AppState = {
|
||||||
started: true,
|
started: true,
|
||||||
size: initialBoardSize,
|
status: 'RUNNING',
|
||||||
board: createBoard(initialBoardSize),
|
size: INITIAL_BOARD_SIZE,
|
||||||
digs: [],
|
numberOfMines: INITIAL_NUMBER_OF_MINES,
|
||||||
|
board: createBoard(INITIAL_BOARD_SIZE, INITIAL_NUMBER_OF_MINES),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startGame = createAction<number>("START_GAME");
|
/* ********* ACTIONS ******************************************************************************** */
|
||||||
export const dig = createAction<Coordinate>("DIG");
|
|
||||||
|
|
||||||
const game = createReducer(defaultAppState, (builder) => {
|
export const startGame = createAction<{ size: number; numberOfMines: number }>('START_GAME');
|
||||||
|
export const dig = createAction<Coordinate>('DIG');
|
||||||
|
|
||||||
|
/* ********* REDUCER ******************************************************************************** */
|
||||||
|
|
||||||
|
const game = createReducer(defaultAppState, builder => {
|
||||||
builder
|
builder
|
||||||
.addCase(startGame, (state, action) => ({
|
.addCase(startGame, (state, { payload: { size, numberOfMines } }) =>
|
||||||
...state,
|
/* Set the initial state for a new game */
|
||||||
started: true,
|
({
|
||||||
board: createBoard(action.payload),
|
...state,
|
||||||
size: action.payload,
|
started: true,
|
||||||
digs: [],
|
status: 'RUNNING',
|
||||||
}))
|
board: createBoard(size, numberOfMines),
|
||||||
|
numberOfMines,
|
||||||
|
size,
|
||||||
|
})
|
||||||
|
)
|
||||||
.addCase(dig, (state, { payload: { row, col } }) => {
|
.addCase(dig, (state, { payload: { row, col } }) => {
|
||||||
|
/* Do nothing if the game is not running */
|
||||||
|
if (state.status !== 'RUNNING') {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find the cell that is clicked on */
|
||||||
|
const cell = state.board.find(boardCell => sameCoordinate(boardCell, { row, col }));
|
||||||
|
|
||||||
|
/* If the clicked cell is a bomb then the game is over */
|
||||||
|
if (cell?.value === 'B') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
status: 'LOST',
|
||||||
|
board: state.board.map(boardCell => ({
|
||||||
|
...boardCell,
|
||||||
|
reveal: boardCell.reveal || boardCell.value === 'B',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Based on the clicked cell find all the neighbouring cells that can be revealed */
|
||||||
|
const revealingCells =
|
||||||
|
cell?.value === 0
|
||||||
|
? [
|
||||||
|
...allNeighbouringCells(
|
||||||
|
state.board,
|
||||||
|
state.size,
|
||||||
|
cell,
|
||||||
|
neighbouringCell => !neighbouringCell.reveal && neighbouringCell.value !== 'B'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
/* Update the board with the state after the selected cell is clicked */
|
||||||
|
const updatedBoard = state.board.map(boardCell =>
|
||||||
|
sameCoordinate(boardCell, { row, col })
|
||||||
|
? { ...boardCell, digged: true, reveal: true }
|
||||||
|
: revealingCells.some(
|
||||||
|
revealingCell => revealingCell.row === boardCell.row && revealingCell.col === boardCell.col
|
||||||
|
)
|
||||||
|
? { ...boardCell, reveal: true }
|
||||||
|
: boardCell
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Check if there are still moves left */
|
||||||
|
const areMovesLeft = updatedBoard.some(boardCell => typeof boardCell.value === 'number' && !boardCell.reveal);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
digs: state.digs[row]?.includes(col)
|
status: areMovesLeft ? 'RUNNING' : 'WON',
|
||||||
? state.digs
|
board: areMovesLeft ? updatedBoard : updatedBoard.map(boardCell => ({ ...boardCell, reveal: true })),
|
||||||
: { ...state.digs, [row]: [...(state.digs[row] || []), col] },
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createGlobalStyle } from "styled-components";
|
import { createGlobalStyle } from 'styled-components';
|
||||||
|
|
||||||
export const GlobalStyle = createGlobalStyle`
|
export const GlobalStyle = createGlobalStyle`
|
||||||
html, body {
|
html, body {
|
||||||
background: black;
|
background: black;
|
||||||
color: white;
|
color: white;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
1
src/types/index.ts
Normal file
1
src/types/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './types';
|
||||||
13
src/types/types.ts
Normal file
13
src/types/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export type GameStatus = 'RUNNING' | 'WON' | 'LOST';
|
||||||
|
export type CellValue = 'B' | number;
|
||||||
|
|
||||||
|
export interface Coordinate {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Cell extends Coordinate {
|
||||||
|
value: CellValue;
|
||||||
|
digged: boolean;
|
||||||
|
reveal: boolean;
|
||||||
|
}
|
||||||
30
src/utils.ts
Normal file
30
src/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export const random = (min: number, max: number): number => min + Math.floor(Math.random() * (max - min));
|
||||||
|
|
||||||
|
export const range = function range(from: number, to: number): number[] {
|
||||||
|
const rangeGenerator = function* rangeGenerator(fromGen: number, toGen: number): Generator<number> {
|
||||||
|
for (let value = fromGen; value < toGen; value += 1) {
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return [...rangeGenerator(from, to)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rangeValues = function rangeValues<T>(from: number, to: number, value: T): T[] {
|
||||||
|
const rangeValuesGenerator = function* rangeValuesGenerator(fromGen: number, toGen: number, valueGen: T): Generator<T> {
|
||||||
|
for (let index = fromGen; index < toGen; index += 1) {
|
||||||
|
yield valueGen;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return [...rangeValuesGenerator(from, to, value)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shuffle = <T extends any>(array: T[]): T[] => {
|
||||||
|
const shuffledArray = [...array];
|
||||||
|
for (let index1 = shuffledArray.length - 1; index1 > 0; index1 -= 1) {
|
||||||
|
const index2 = Math.floor(Math.random() * (index1 + 1));
|
||||||
|
[shuffledArray[index1], shuffledArray[index2]] = [shuffledArray[index2], shuffledArray[index1]];
|
||||||
|
}
|
||||||
|
return shuffledArray;
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES5",
|
||||||
"lib": ["DOM", "ES2017"],
|
"lib": ["DOM", "ES2017"],
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
// "baseUrl": "./src",
|
// "baseUrl": "./src",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user