initial implementation of arkanoid.ts

This commit is contained in:
2021-09-21 20:13:51 +02:00
commit 814c564a85
15 changed files with 20784 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true
quote=singleQuote
[*.md]
trim_trailing_whitespace = false
+267
View File
@@ -0,0 +1,267 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['plugin:react/recommended', 'eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
settings: {
react: {
version: 'detect',
},
},
rules: {
// note you must disable the base rule as it can report incorrect errors
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-empty-function': 'error',
'react/prop-types': 0,
'accessor-pairs': 'error',
'array-bracket-newline': 'error',
'array-bracket-spacing': 'error',
'array-callback-return': 'error',
'array-element-newline': 0,
'arrow-body-style': 'error',
'arrow-parens': 0,
'arrow-spacing': 'error',
'block-scoped-var': 'error',
'block-spacing': 'error',
'brace-style': 'error',
'callback-return': 'error',
camelcase: 0,
'capitalized-comments': 0,
'class-methods-use-this': 'error',
'comma-dangle': 0,
'comma-spacing': 'error',
'comma-style': 'error',
complexity: 'error',
'computed-property-spacing': 'error',
'consistent-return': 'error',
'consistent-this': 'error',
curly: 'error',
'default-case': 'error',
'default-case-last': 'error',
'default-param-last': 'error',
'dot-location': 0,
'dot-notation': 'error',
'eol-last': 'error',
eqeqeq: 'error',
'func-call-spacing': 'error',
'func-name-matching': 'error',
'func-names': 'error',
'func-style': 'error',
'function-call-argument-newline': 0,
'function-paren-newline': 0,
'generator-star-spacing': 0,
'global-require': 'error',
'grouped-accessor-pairs': 'error',
'guard-for-in': 'error',
'handle-callback-err': 'error',
'id-blacklist': 'error',
'id-denylist': 'error',
'id-length': 0,
'id-match': 'error',
'implicit-arrow-linebreak': 0,
indent: 0,
// indent: [
// 'error',
// 2,
// {
// SwitchCase: 1,
// MemberExpression: 1,
// CallExpression: { arguments: 1 },
// offsetTernaryExpressions: true,
// flatTernaryExpressions: true,
// ignoredNodes: ['ConditionalExpression'],
// },
// ],
'indent-legacy': 0,
'init-declarations': 'error',
'jsx-quotes': 'error',
'key-spacing': 'error',
'keyword-spacing': 'error',
'line-comment-position': 'error',
'linebreak-style': 'error',
'lines-around-comment': 0,
'lines-around-directive': 'error',
'lines-between-class-members': 'error',
'max-classes-per-file': 'error',
'max-depth': 'error',
'max-len': ['error', 180],
'max-lines': 'error',
'max-lines-per-function': 0,
'max-nested-callbacks': 'error',
'max-params': 0,
'max-statements': 0,
'max-statements-per-line': 'error',
'multiline-comment-style': 0,
'multiline-ternary': 0,
'new-cap': 0,
'new-parens': 'error',
'newline-after-var': 0,
'newline-before-return': 0,
'newline-per-chained-call': 0,
'no-alert': 'error',
'no-array-constructor': 'error',
'no-await-in-loop': 'error',
'no-bitwise': 'error',
'no-buffer-constructor': 'error',
'no-caller': 'error',
'no-catch-shadow': 'error',
'no-confusing-arrow': 0,
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-constructor-return': 'error',
'no-continue': 0,
'no-div-regex': 'error',
'no-duplicate-imports': 'error',
'no-else-return': 'error',
'no-empty-function': 0,
'no-eq-null': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-label': 'error',
'no-extra-parens': 0,
'no-floating-decimal': 'error',
'no-implicit-coercion': 'error',
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-inline-comments': 'error',
'no-invalid-this': 'error',
'no-iterator': 'error',
'no-label-var': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-lonely-if': 'error',
'no-loop-func': 'error',
'no-loss-of-precision': 'error',
'no-magic-numbers': 0,
'no-mixed-operators': 0,
'no-mixed-requires': 'error',
'no-multi-assign': 'error',
'no-multi-spaces': 'error',
'no-multi-str': 'error',
'no-multiple-empty-lines': 'error',
'no-native-reassign': 'error',
'no-negated-condition': 'error',
'no-negated-in-lhs': 'error',
'no-nested-ternary': 0,
'no-new': 'error',
'no-new-func': 'error',
'no-new-object': 'error',
'no-new-require': 'error',
'no-new-wrappers': 'error',
'no-octal-escape': 'error',
'no-param-reassign': 'error',
'no-path-concat': 'error',
'no-plusplus': 'error',
'no-process-env': 'error',
'no-process-exit': 'error',
'no-promise-executor-return': 'error',
'no-proto': 'error',
'no-restricted-exports': 'error',
'no-restricted-globals': 'error',
'no-restricted-imports': 'error',
'no-restricted-modules': 'error',
'no-restricted-properties': 'error',
'no-restricted-syntax': 'error',
'no-return-assign': 'error',
'no-return-await': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-shadow': 0,
'no-spaced-func': 'error',
'no-sync': 'error',
'no-tabs': 'error',
'no-template-curly-in-string': 'error',
'no-ternary': 0,
'no-throw-literal': 'error',
'no-trailing-spaces': 'error',
'no-undef-init': 0,
'no-undefined': 0,
'no-underscore-dangle': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': 'error',
'no-unreachable-loop': 'error',
'no-unused-expressions': 'error',
'no-useless-backreference': 'error',
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 0,
'no-useless-rename': 'error',
'no-useless-return': 'error',
'no-var': 'error',
'no-void': 0,
'no-warning-comments': 'error',
'no-whitespace-before-property': 'error',
'nonblock-statement-body-position': 'error',
'object-curly-newline': 'error',
'object-curly-spacing': 0,
'object-property-newline': 0,
'object-shorthand': 'error',
'one-var': 0,
'one-var-declaration-per-line': 0,
'operator-assignment': 'error',
'operator-linebreak': 'error',
'padded-blocks': 0,
'padding-line-between-statements': 'error',
'prefer-arrow-callback': 'error',
'prefer-const': 'error',
'prefer-destructuring': 'error',
'prefer-exponentiation-operator': 'error',
'prefer-named-capture-group': 'error',
'prefer-numeric-literals': 'error',
'prefer-object-spread': 'error',
'prefer-promise-reject-errors': 'error',
'prefer-reflect': 'error',
'prefer-regex-literals': 'error',
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'quote-props': ['error', 'as-needed'],
quotes: ['error', 'single'],
radix: 'error',
'require-atomic-updates': 'error',
'require-await': 'error',
'require-jsdoc': 0,
'require-unicode-regexp': 'error',
'rest-spread-spacing': 'error',
semi: 'error',
'semi-spacing': 'error',
'semi-style': 'error',
'sort-imports': 0,
'sort-keys': 0,
'sort-vars': 0,
'space-before-blocks': 'error',
'space-before-function-paren': 0,
'space-in-parens': 'error',
'space-infix-ops': 'error',
'space-unary-ops': 'error',
'spaced-comment': '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',
},
};
+3
View File
@@ -0,0 +1,3 @@
dist
node_modules
.parcel-cache
+12
View 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"
}
+20184
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"scripts": {
"start": "parcel serve --host 0.0.0.0 src/index.htm",
"type": "tsc --noEmit",
"lint": "eslint --ext .ts,.tsx,.js,.jsx ./src",
"lint:fix": "eslint --ext .ts,.tsx,.js,.jsx ./src --fix",
"prettier": "prettier --write 'src/**/*.{ts,tsx,json,md,js,scss}'"
},
"devDependencies": {
"@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",
"parcel": "^2.0.0-rc.0",
"tslib": "^2.0.3",
"typescript": "^4.0.5"
},
"dependencies": {
"eslint-plugin-react": "^7.26.0"
}
}
+45
View File
@@ -0,0 +1,45 @@
import { board } from './board';
import { puck } from './puck';
import { Position, Size, Element, Velocity } from './types';
export const ball = ((): Element => {
const instance = document.createElement('div');
const { style } = instance;
const size: Size = { width: 20, height: 20 };
const vel: Velocity = { x: 5, y: 5 };
const pos = new Position(Math.round(board.size.width / 2), Math.round(board.size.height / 2));
style.position = 'absolute';
style.transition = 'transform linear 20ms';
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
style.width = `${size.width}px`;
style.height = `${size.height}px`;
style.borderRadius = '50%';
style.backgroundColor = '#ff5656';
const update = () => {
if (!board.isInBoundsX(pos.x + vel.x, size.width)) {
vel.x *= -1;
}
if (!board.isInBoundsY(pos.y + vel.y, size.height)) {
vel.y *= -1;
}
pos.x += vel.x;
pos.y += vel.y;
if (puck.intersects(ball)) {
vel.y = Math.abs(vel.y) * -1;
}
if (pos.y + size.height >= board.size.height) {
pos.move(Math.round(board.size.width / 2), Math.round(board.size.height / 2));
}
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
};
return {
size,
pos,
instance,
update,
};
})();
+26
View File
@@ -0,0 +1,26 @@
import { Position, Size } from './types';
export const board = (() => {
const instance = document.createElement('div');
const { style } = instance;
const boardSize: Size = { width: 433, height: 800 };
style.width = `${boardSize.width}px`;
style.height = `${boardSize.height}px`;
style.backgroundColor = '#2d2d2d';
style.margin = '40px auto';
const isInBoundsX = (x: number, w: number): boolean => x >= 0 && x + w <= boardSize.width;
const isInBoundsY = (y: number, h: number): boolean => y >= 0 && y + h <= boardSize.height;
const isInBounds = (pos: Position, size: Size): boolean =>
isInBoundsX(pos.x, size.width) && isInBoundsY(pos.y, size.height);
return {
size: boardSize,
instance,
isInBoundsX,
isInBoundsY,
isInBounds,
};
})();
+1
View File
@@ -0,0 +1 @@
export const game = { paused: false };
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Arkanoid</title>
<style>
html, body {
background-color: #1d1d1d;
color: white;
font-family: sans-serif;
font-size: 16px;
}
</style>
<script src="index.ts" type="module"></script>
</head>
<body>
<div id="game"></div>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
import { keyboard, Keys } from './keyboard';
import { board } from './board';
import { puck } from './puck';
import { ball } from './ball';
import { game } from './game';
const triggerGameLoop = () =>
requestAnimationFrame(() => {
if (keyboard.isKeyPressed(Keys.P)) {
game.paused = !game.paused;
}
if (!game.paused) {
puck.update();
ball.update();
}
triggerGameLoop();
});
const run = (): void => {
const gameElement = document.querySelector('#game');
if (!gameElement) {
return console.error('No dom element with id game found');
}
gameElement.appendChild(board.instance);
board.instance.appendChild(puck.instance);
board.instance.appendChild(ball.instance);
return void triggerGameLoop();
};
document.addEventListener('DOMContentLoaded', run);
+22
View File
@@ -0,0 +1,22 @@
export enum Keys {
ARROW_RIGHT = 'ArrowRight',
ARROW_LEFT = 'ArrowLeft',
P = 'p',
}
const pressedKeys: Partial<Record<Keys, boolean>> = {};
document.addEventListener('keydown', e => {
pressedKeys[e.key as Keys] = true;
});
document.addEventListener('keyup', e => {
pressedKeys[e.key as Keys] = false;
});
export const keyboard = {
pressedKeys,
isKeyPressed(key: Keys): boolean {
return Boolean(pressedKeys[key]);
},
};
+60
View File
@@ -0,0 +1,60 @@
import { board } from './board';
import { keyboard, Keys } from './keyboard';
import { Position, Size, Element } from './types';
export interface Puck extends Element {
intersects(element: Element): boolean;
}
export const puck = ((): Puck => {
const instance = document.createElement('div');
const { style } = instance;
const bottomMargin = 30;
const size: Size = { width: 70, height: 20 };
const pos = new Position(0, board.size.height - size.height - bottomMargin);
const stepSize = 10;
style.position = 'absolute';
style.zIndex = '1';
style.transition = 'transform linear 20ms';
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
style.width = `${size.width}px`;
style.height = `${size.height}px`;
style.borderRadius = '10%';
style.backgroundColor = '#36afe8';
const update = () => {
if (keyboard.isKeyPressed(Keys.ARROW_RIGHT) && board.isInBounds(pos.clone().addX(stepSize), size)) {
pos.addX(stepSize);
} else if (keyboard.isKeyPressed(Keys.ARROW_LEFT) && board.isInBounds(pos.clone().addX(-stepSize), size)) {
pos.addX(-stepSize);
} else {
return;
}
style.transform = `translate(${pos.x}px, ${pos.y}px)`;
};
const intersects = (element: Element): boolean => {
let yIntersects = false;
let xIntersects = false;
if (element.pos.y + element.size.height >= pos.y && element.pos.y + element.size.height <= pos.y + 1) {
yIntersects = true;
}
if (element.pos.x > pos.x && element.pos.x < pos.x + size.width) {
xIntersects = true;
}
if (element.pos.x + element.size.width > pos.x && element.pos.x + element.size.width < pos.x + size.width) {
xIntersects = true;
}
return yIntersects && xIntersects;
};
return {
size,
pos,
instance,
update,
intersects,
};
})();
+54
View File
@@ -0,0 +1,54 @@
export interface Velocity {
x: number;
y: number;
}
export class Position {
constructor(public x: number, public y: number) {}
clone(): Position {
return new Position(this.x, this.y);
}
moveX(x: number): Position {
this.x = x;
return this;
}
moveY(y: number): Position {
this.y = y;
return this;
}
move(x: number, y: number): Position {
this.x = x;
this.y = y;
return this;
}
addX(x: number): Position {
return this.add(x, 0);
}
addY(y: number): Position {
return this.add(0, y);
}
add(x: number, y: number): Position {
this.x += x;
this.y += y;
return this;
}
}
export interface Size {
width: number;
height: number;
}
export interface Element {
size: Size;
pos: Position;
instance: HTMLElement;
update(): void;
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["DOM", "ES2017"],
"jsx": "react",
"module": "esnext",
"sourceMap": true,
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"noEmit": true,
"importHelpers": true,
"typeRoots": ["./src/types", "./types", "./node_modules/@types"]
},
"include": ["src"],
"exclude": ["node_modules"]
}