From fc2c8ac1aa757181b467727eb574ffae480d9826 Mon Sep 17 00:00:00 2001 From: Jakub Pelc <jakub.pelc@email.cz> Date: Tue, 25 Mar 2025 22:16:43 +0100 Subject: [PATCH] =?UTF-8?q?P=C5=99id=C3=A1na=20typescriptov=C3=A1=20implem?= =?UTF-8?q?entace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asteracer-typescript/.gitignore | 1 + asteracer-typescript/README.md | 5 + asteracer-typescript/index.ts | 34 +++ asteracer-typescript/package-lock.json | 236 +++++++++++++++++ asteracer-typescript/package.json | 17 ++ asteracer-typescript/src/asteracer/grid.ts | 72 ++++++ .../src/asteracer/parse_map.ts | 103 ++++++++ .../src/asteracer/simulation.ts | 241 ++++++++++++++++++ asteracer-typescript/src/asteracer/types.ts | 45 ++++ asteracer-typescript/src/util.ts | 3 + asteracer-typescript/tsconfig.json | 16 ++ 11 files changed, 773 insertions(+) create mode 100644 asteracer-typescript/.gitignore create mode 100644 asteracer-typescript/README.md create mode 100644 asteracer-typescript/index.ts create mode 100644 asteracer-typescript/package-lock.json create mode 100644 asteracer-typescript/package.json create mode 100644 asteracer-typescript/src/asteracer/grid.ts create mode 100644 asteracer-typescript/src/asteracer/parse_map.ts create mode 100644 asteracer-typescript/src/asteracer/simulation.ts create mode 100644 asteracer-typescript/src/asteracer/types.ts create mode 100644 asteracer-typescript/src/util.ts create mode 100644 asteracer-typescript/tsconfig.json diff --git a/asteracer-typescript/.gitignore b/asteracer-typescript/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/asteracer-typescript/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/asteracer-typescript/README.md b/asteracer-typescript/README.md new file mode 100644 index 0000000..44c9e01 --- /dev/null +++ b/asteracer-typescript/README.md @@ -0,0 +1,5 @@ +Použití: + +- Instalujte si Node >= 22 (`nvm use 22` pokud máte nvm) (je potřeba pro přímé spouštění typescriptu bez kompilátoru) +- `npm install` +- `npm run app [název souboru mapy] [název souboru s instrukcemi]` diff --git a/asteracer-typescript/index.ts b/asteracer-typescript/index.ts new file mode 100644 index 0000000..e3caf96 --- /dev/null +++ b/asteracer-typescript/index.ts @@ -0,0 +1,34 @@ +import { readFileSync } from "fs"; +import { checkInstructions, simulate } from "./src/asteracer/simulation"; +import { parseInstructions, parseMap } from "./src/asteracer/parse_map"; + +const worldFileName = process.argv[2]; +const instructionsFileName = process.argv[3]; + +if (!worldFileName || !instructionsFileName) { + console.log("Použití: npm run app [název souboru mapy] [název souboru s instrukcemi]"); + process.exit(); +} + +const worldFileText = readFileSync(worldFileName, { encoding: 'utf8', flag: 'r' }); +const instructionsFileText = readFileSync(instructionsFileName, { encoding: 'utf8', flag: 'r' }); + +const world = parseMap(worldFileText); +const instructions = parseInstructions(instructionsFileText); + +try { + checkInstructions(instructions); +} catch (e) { + console.error(e.message); + process.exit(); +} + +const simulationResult = await simulate(world, instructions); + +if (simulationResult.messages) { + for (const msg of simulationResult.messages) { + console.log(msg); + } +} else { + console.log("Závod úspěšně dokončen!"); +} diff --git a/asteracer-typescript/package-lock.json b/asteracer-typescript/package-lock.json new file mode 100644 index 0000000..287f821 --- /dev/null +++ b/asteracer-typescript/package-lock.json @@ -0,0 +1,236 @@ +{ + "name": "asteracer-simulation", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "asteracer-simulation", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.13.13", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/asteracer-typescript/package.json b/asteracer-typescript/package.json new file mode 100644 index 0000000..b53622c --- /dev/null +++ b/asteracer-typescript/package.json @@ -0,0 +1,17 @@ +{ + "name": "asteracer-simulation", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "app": "node --experimental-strip-types --experimental-specifier-resolution=node --loader ts-node/esm ./index.ts" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "^22.13.13", + "ts-node": "^10.9.2", + "typescript": "^5.7.3" + } +} diff --git a/asteracer-typescript/src/asteracer/grid.ts b/asteracer-typescript/src/asteracer/grid.ts new file mode 100644 index 0000000..3c48d6c --- /dev/null +++ b/asteracer-typescript/src/asteracer/grid.ts @@ -0,0 +1,72 @@ +import { Aabb, CircleId, Point } from "./types"; + +export interface IGrid { + //queryAlongLine(a: Point, b: Point, radius: number): Circle[]; + queryCircle(p: Point, radius: number): CircleId[]; +} + +/** + * Stupid "acceleration" structure that just iterates over everything. + */ +export class Grid implements IGrid { + private _circles: CircleId[]; + + constructor(items: CircleId[]) { + this._circles = items; + } + + public queryAlongLine(a: Point, b: Point, radius: number): CircleId[] { + const intersections: CircleId[] = []; + + for (const c of this._circles) { + const dist = pointLineSegmentDistance(a, b, c); + if (dist < (c.radius + radius) * 1.5) { + intersections.push(c); + } + } + + return intersections; + } + + public queryCircle(p: Point, radius: number): CircleId[] { + const intersections: CircleId[] = []; + + for (let i = 0; i < this._circles.length; i++) { + const c = this._circles[i]; + const dx = (c.x - p.x) | 0; + const dy = (c.y - p.y) | 0; + const distSq = dx * dx + dy * dy; + const threshold = (radius + c.radius + 100) | 0; + if (distSq <= threshold * threshold) { + intersections.push(c); + } + } + + return intersections; + } +} + +function distanceF64(a: Point, b: Point): number { + const dx = a.x - b.x; + const dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy); +} + +function pointLineSegmentDistance(lineA: Point, lineB: Point, point: Point): number { + const dirX = lineB.x - lineA.x; + const dirY = lineB.y - lineA.y; + const relativePointX = point.x - lineA.x; + const relativePointY = point.y - lineA.y; + const lineLength = Math.sqrt(dirX * dirX + dirY * dirY); + + const distAlongLineClosestPoint = (dirX * relativePointX + dirY * relativePointY) / lineLength; + const clampedDistAlongLineClosestPoint = Math.min(Math.max(distAlongLineClosestPoint, 0), lineLength); + + const closest = { + x: lineA.x + dirX * clampedDistAlongLineClosestPoint, + y: lineA.y + dirY * clampedDistAlongLineClosestPoint, + }; + + const distClosest = distanceF64(closest, point); + return distClosest; +} diff --git a/asteracer-typescript/src/asteracer/parse_map.ts b/asteracer-typescript/src/asteracer/parse_map.ts new file mode 100644 index 0000000..1c9cc80 --- /dev/null +++ b/asteracer-typescript/src/asteracer/parse_map.ts @@ -0,0 +1,103 @@ +import { Point, World } from "./types"; + +class Parser { + private _split: string[]; + private _index: number = 0; + + constructor(file: string) { + this._split = file.split(/\s+/); + } + + public readInt(): number { + const int = parseInt(this._split[this._index]) | 0; + this._index++; + return int; + } + + public end(): boolean { + return this._index === this._split.length - 1; + } +} + +export function parseMap(file: string): World { + const input = new Parser(file); + + const world: World = { + shipStartX: input.readInt(), + shipStartY: input.readInt(), + shipRadius: input.readInt(), + minX: input.readInt(), + minY: input.readInt(), + maxX: input.readInt(), + maxY: input.readInt(), + asteroids: [], + goals: [], + }; + + const asteroidCount = input.readInt(); + + for (let i = 0; i < asteroidCount; i++) { + world.asteroids.push({ + x: input.readInt(), + y: input.readInt(), + radius: input.readInt(), + id: i, + type: "asteroid", + }); + } + + const goalCount = input.readInt(); + + for (let i = 0; i < goalCount; i++) { + world.goals.push({ + x: input.readInt(), + y: input.readInt(), + radius: input.readInt(), + id: i, + type: "goal", + }); + } + + return world; +} + +export function parseInstructions(file: string): Point[] { + const input = new Parser(file); + + const instructions: Point[] = []; + + const instructionCount = input.readInt(); + + for (let i = 0; i < instructionCount; i++) { + if (input.end()) { + throw new Error("V souboru je méně instrukcí, než kolik udává jeho první řádek!"); + } + instructions.push({ + x: input.readInt(), + y: input.readInt(), + }); + } + + return instructions; +} + +export function parseLog(file: string): [Point[], Point[]] { + const expectedPositions: Point[] = []; + const expectedVelocity: Point[] = []; + + const input = new Parser(file); + + while(!input.end()) { + expectedPositions.push({ + x: input.readInt(), + y: input.readInt(), + }); + expectedVelocity.push({ + x: input.readInt(), + y: input.readInt(), + }); + input.readInt(); // ignore goals + } + + return [expectedPositions, expectedVelocity]; +} diff --git a/asteracer-typescript/src/asteracer/simulation.ts b/asteracer-typescript/src/asteracer/simulation.ts new file mode 100644 index 0000000..63d3a2f --- /dev/null +++ b/asteracer-typescript/src/asteracer/simulation.ts @@ -0,0 +1,241 @@ +import { timeout } from "../util"; +import { Grid, IGrid } from "./grid"; +import { Aabb, CircleId, Point, SimulationResult, TickResult, World } from "./types"; + +const dragSlowdownFraction = [9, 10]; +const collisionSlowdownFraction = [1, 2]; +const maxCollisionSubticks = 5; + +function tick(gridAsteroids: IGrid, gridGoals: IGrid, worldAabb: Aabb, shipRadius: number, shipPositionStart: Point, shipVelocityStart: Point, instruction: Point): TickResult { + let velocityX = shipVelocityStart.x | 0; + let velocityY = shipVelocityStart.y | 0; + + const r = shipRadius | 0; + + // Zpomalení + velocityX = ((velocityX * (dragSlowdownFraction[0] | 0)) / (dragSlowdownFraction[1] | 0)) | 0; + velocityY = ((velocityY * (dragSlowdownFraction[0] | 0)) / (dragSlowdownFraction[1] | 0)) | 0; + + // Instrukce + velocityX += instruction.x | 0; + velocityY += instruction.y | 0; + + // Posun + let positionX = (shipPositionStart.x | 0) + velocityX; + let positionY = (shipPositionStart.y | 0) + velocityY; + + // Kolize + let hadAnyCollision = false; + + for (let subtick = 0; subtick < maxCollisionSubticks; subtick++) { + let collidedThisSubtick = false; + + const asteroidCandidates = gridAsteroids.queryCircle({ + x: positionX, + y: positionY, + }, r); + + for (let i = 0; i < asteroidCandidates.length; i++) { + const c = asteroidCandidates[i]; + const dx = (positionX - c.x) | 0; // od středu asteroidu ke středu lodi + const dy = (positionY - c.y) | 0; + const distSq = dx * dx + dy * dy; + const dist = Math.sqrt(distSq) | 0; + const threshold = (r + c.radius) | 0; + if (dist > threshold) { + // Kolize nenastala + continue; + } + + // Kolidujeme + const pushBy = (c.radius + r) - dist; + positionX += (((pushBy * dx) | 0) / dist) | 0; + positionY += (((pushBy * dy) | 0) / dist) | 0; + + collidedThisSubtick = true; + break; + } + + // Kolize s okraji + if (positionX - r < worldAabb.minX) { + positionX = (worldAabb.minX | 0) + r; + collidedThisSubtick = true; + } + if (positionY - r < worldAabb.minY) { + positionY = (worldAabb.minY | 0) + r; + collidedThisSubtick = true; + } + if (positionX + r > worldAabb.maxX) { + positionX = (worldAabb.maxX | 0) - r; + collidedThisSubtick = true; + } + if (positionY + r > worldAabb.maxY) { + positionY = (worldAabb.maxY | 0) - r; + collidedThisSubtick = true; + } + + if (collidedThisSubtick) { + hadAnyCollision = true; + } else { + break; + } + } + + // Zpomalení v důsledku kolize + if (hadAnyCollision) { + velocityX = ((velocityX * (collisionSlowdownFraction[0] | 0)) / (collisionSlowdownFraction[1] | 0)) | 0; + velocityY = ((velocityY * (collisionSlowdownFraction[0] | 0)) / (collisionSlowdownFraction[1] | 0)) | 0; + } + + // Kolize s cíli + let goalsIntersected: CircleId[] = []; + + const goalCandidates = gridGoals.queryCircle({ + x: positionX, + y: positionY, + }, r); + + for (let i = 0; i < goalCandidates.length; i++) { + const c = goalCandidates[i]; + const dx = (positionX - c.x) | 0; // od středu cíle ke středu lodi + const dy = (positionY - c.y) | 0; + const distSq = dx * dx + dy * dy; + const dist = Math.sqrt(distSq) | 0; + const threshold = (r + c.radius) | 0; + if (dist > threshold) { + // Kolize nenastala + continue; + } + + // Kolidujeme + goalsIntersected.push(c); + } + + return { + shipPositionStart: { + x: shipPositionStart.x, + y: shipPositionStart.y, + }, + shipPositionEnd: { + x: positionX, + y: positionY, + }, + shipVelocityStart: { + x: shipVelocityStart.x, + y: shipVelocityStart.y, + }, + shipVelocityEnd: { + x: velocityX, + y: velocityY, + }, + goalsIntersected, + }; +} + +/** + * Throws if any instruction is invalid (too large acceleration). + */ +export function checkInstructions(instructions: Point[]): void { + for (const i of instructions) { + const x = i.x | 0; + const y = i.y | 0; + const lenSq = x * x + y * y; + if (lenSq > 127 * 127) { + throw new Error(`Instrukce x=${i.x} y=${i.y} přesáhla povolené zrychlení! (${Math.sqrt(lenSq)} > 127)`); + } + } +} + +export async function simulate(world: World, instructions: Point[], periodicYield?: boolean, onYield?: (processedInstructions: number) => void): Promise<SimulationResult> { + const gridAsteroids = new Grid(world.asteroids); + const gridGoals = new Grid(world.goals); + + const ticks: TickResult[] = []; + let latestPosition: Point = { + x: world.shipStartX, + y: world.shipStartY, + }; + let latestVelocity: Point = { + x: 0, + y: 0, + }; + let tickCount = 0; + let goalsReached = 0; + let reachedGoalIds: Set<number> = new Set(); + let allGoalsReachedTick: number = undefined; + + for (let index = 0; index < instructions.length; index++) { + const instruction = instructions[index]; + const result = tick(gridAsteroids, gridGoals, world, world.shipRadius, latestPosition, latestVelocity, instruction); + latestPosition.x = result.shipPositionEnd.x; + latestPosition.y = result.shipPositionEnd.y; + latestVelocity.x = result.shipVelocityEnd.x; + latestVelocity.y = result.shipVelocityEnd.y; + tickCount++; + ticks.push(result); + for (const goal of result.goalsIntersected) { + if (!reachedGoalIds.has(goal.id)) { + reachedGoalIds.add(goal.id) + goalsReached++; + } + } + if (!allGoalsReachedTick && goalsReached >= world.goals.length) { + allGoalsReachedTick = index; + } + + if (periodicYield && index > 0 && index % 5000 === 0) { + if (onYield) { + onYield(index); + } + await timeout(0); + } + } + + const messages: string[] = []; + + if (!allGoalsReachedTick && world.goals.length > 0) { + messages.push("Nebyly dosaženy všechny cíle!"); + } + + if (allGoalsReachedTick && allGoalsReachedTick < instructions.length - 1) { + messages.push("Vaše instrukce pokračují i po dosažení posledního cíle! Instrukce po dosažení posledního cíle můžete bezpečně smazat a tím si vylepšit čas."); + } + + return { + goalsReached: reachedGoalIds, + ticksExecuted: tickCount, + tickResults: ticks, + messages + }; +} + +export function generateLog(world: World, result: SimulationResult): string { + const chunks: string[] = []; + + let goalStates = []; + + for (let i = 0; i < world.goals.length; i++) { + goalStates.push(0); + } + + for (const tick of result.tickResults) { + for (const goal of tick.goalsIntersected) { + goalStates[goal.id] = 1; + } + + chunks.push(tick.shipPositionEnd.x.toString()); + chunks.push(" "); + chunks.push(tick.shipPositionEnd.y.toString()); + chunks.push(" "); + chunks.push(tick.shipVelocityEnd.x.toString()); + chunks.push(" "); + chunks.push(tick.shipVelocityEnd.y.toString()); + chunks.push(" "); + for (let i = 0; i < goalStates.length; i++) { + chunks.push(goalStates[i].toString()); + } + chunks.push("\n"); + } + + return chunks.join(""); +} diff --git a/asteracer-typescript/src/asteracer/types.ts b/asteracer-typescript/src/asteracer/types.ts new file mode 100644 index 0000000..d9dd711 --- /dev/null +++ b/asteracer-typescript/src/asteracer/types.ts @@ -0,0 +1,45 @@ +export type Aabb = { + minX: number; + minY: number; + maxX: number; + maxY: number; +}; + +export type Circle = { + x: number; + y: number; + radius: number; +}; + +export type CircleId = Circle &{ + id: number; + type: 'asteroid' | 'goal'; +} + +export type World = Aabb & { + shipStartX: number; + shipStartY: number; + shipRadius: number; + asteroids: CircleId[]; + goals: CircleId[]; +}; + +export type Point = { + x: number; + y: number; +}; + +export type TickResult = { + shipPositionStart: Point; + shipVelocityStart: Point; + shipPositionEnd: Point; + shipVelocityEnd: Point; + goalsIntersected: CircleId[]; +}; + +export type SimulationResult = { + ticksExecuted: number; + goalsReached: Set<number>; + tickResults: TickResult[]; + messages: string[]; +}; diff --git a/asteracer-typescript/src/util.ts b/asteracer-typescript/src/util.ts new file mode 100644 index 0000000..eca72af --- /dev/null +++ b/asteracer-typescript/src/util.ts @@ -0,0 +1,3 @@ +export function timeout(ms: number): Promise<void> { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/asteracer-typescript/tsconfig.json b/asteracer-typescript/tsconfig.json new file mode 100644 index 0000000..acdae78 --- /dev/null +++ b/asteracer-typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "noImplicitAny": true, + "lib": [ "dom", "esnext" ], + "module": "esnext", + "target": "ES2017", + "strict": false, + "moduleDetection": "force", + "moduleResolution": "node", + }, + "exclude": [ + "node_modules", + "static" + ], + "include": ["src/**/*.ts"] +}