commit 491e5b97bb9343b3f210fcbc4df85edf76c1879e Author: tmont Date: Mon Jun 7 10:42:00 2021 -0700 tetris? diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6432642 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.idea + diff --git a/index.html b/index.html new file mode 100644 index 0000000..8ffe8e1 --- /dev/null +++ b/index.html @@ -0,0 +1,225 @@ + + + + + Tetrissimo + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e3a544 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "tetrissimo", + "version": "0.0.0", + "license": "WTFPL", + "scripts": { + "build": "tsc", + "watch": "tsc --watch --preserveWatchOutput", + "start": "serve -l 19000 ." + }, + "devDependencies": { + "serve": "*", + "typescript": "4.0.3" + } +} diff --git a/requirejs-polyfill.js b/requirejs-polyfill.js new file mode 100644 index 0000000..5542ff9 --- /dev/null +++ b/requirejs-polyfill.js @@ -0,0 +1,52 @@ +((window) => { + const moduleCache = {}; + const exportsCache = {}; + const exportsPlaceholder = '!exports'; + + function define(name, deps, fn) { + if (Array.isArray(name)) { + if (typeof (deps) === 'function') { + const resolvedDeps = require(name); + deps.apply(null, resolvedDeps); + } + return; + } + + moduleCache[name] = {deps, fn}; + } + + function require(names) { + return (names || []) + .map((name) => { + if (name in exportsCache) { + return exportsCache[name]; + } + if (name === 'exports') { + return exportsPlaceholder; + } + + const mod = moduleCache[name]; + if (!mod) { + throw new Error(`Unknown module "${name}"`); + } + + exportsCache[name] = {}; + const args = require(mod.deps); + const index = args.findIndex(x => x === exportsPlaceholder); + if (index !== -1) { + args[index] = exportsCache[name]; + } + mod.fn.apply(null, args); + const moduleKey = '__$module'; + if (exportsCache[name][moduleKey]) { + exportsCache[name] = exportsCache[name][moduleKey]; + } + return exportsCache[name]; + }); + } + + window.require = exportsCache.require = require; + window.define = define; + window.exportsCache = exportsCache; + window.moduleCache = moduleCache; +})(window); diff --git a/tetrissimo.js b/tetrissimo.js new file mode 100644 index 0000000..de3a766 --- /dev/null +++ b/tetrissimo.js @@ -0,0 +1,555 @@ +const nope = (x) => { }; +const getSize = (value, options) => value * options.pixelSize; +const drawBorderedRect = (position, context, options) => { + const borderWidth = options.borderWidth; + if (!borderWidth) { + return; + } + const actualLineWidth = getSize(borderWidth, options); + context.lineWidth = actualLineWidth; + context.strokeStyle = 'white'; + context.strokeRect(getSize(position.x, options) - Math.round(actualLineWidth / 2), getSize(position.y, options) - Math.round(actualLineWidth / 2), getSize(position.width, options) + actualLineWidth, getSize(position.height, options) + actualLineWidth); +}; +class TetrissimoLevel { + constructor(order, speed) { + this.order = order; + this.movementsPerSecond = speed; + } +} +TetrissimoLevel.level1 = new TetrissimoLevel(1, 0.5); +TetrissimoLevel.level2 = new TetrissimoLevel(2, 0.75); +TetrissimoLevel.level3 = new TetrissimoLevel(2, 1); +TetrissimoLevel.level4 = new TetrissimoLevel(2, 1.5); +TetrissimoLevel.level5 = new TetrissimoLevel(2, 2); +TetrissimoLevel.level6 = new TetrissimoLevel(2, 3); +TetrissimoLevel.level7 = new TetrissimoLevel(2, 4); +TetrissimoLevel.level8 = new TetrissimoLevel(2, 6); +TetrissimoLevel.level9 = new TetrissimoLevel(2, 8); +TetrissimoLevel.level10 = new TetrissimoLevel(2, 10); +class TetrissimoGame { + constructor(ui) { + this.startedAt = null; + this.pausedAt = null; + this.finishedAt = null; + this.state = 'not-started'; + this.fps = 60; + this.lastFrameTimes = []; + this.pieces = []; + this.currentLevel = TetrissimoLevel.level1; + this.msUntilNextTick = 0; + this.lastTick = new Date(); + this.lastUpdate = new Date(); + this.ui = ui; + } + getState() { + return this.state; + } + setTargetFps(fps) { + this.fps = fps; + } + setLevel(level) { + this.currentLevel = level; + } + getCurrentFps() { + switch (this.state) { + case 'running': + if (this.lastFrameTimes.length <= 1) { + return 0; + } + const firstFrame = this.lastFrameTimes[0]; + const lastFrame = this.lastFrameTimes[this.lastFrameTimes.length - 1]; + const elapsedMs = lastFrame - firstFrame; + const avgMsPerFrame = (elapsedMs / this.lastFrameTimes.length); + return 1000 / avgMsPerFrame; + case 'paused': + case 'not-started': + case 'finished': + return 0; + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + performPieceAction(action) { + if (this.state !== 'running') { + return; + } + const piece = this.pieces.filter(piece => piece.state === 'in-play')[0]; + if (!piece) { + // nothing to do, no pieces in play + return; + } + const movementIncrement = 1; + switch (action.type) { + case 'moveLeft': + this.moveIfValid(piece, -movementIncrement, 0); + break; + case 'moveRight': + this.moveIfValid(piece, movementIncrement, 0); + break; + case 'moveDown': + if (!this.moveIfValid(piece, 0, movementIncrement)) { + // make fixed to bottom of play area + piece.setFixed(); + } + break; + case 'rotateLeft': + // TODO ensure it's a valid move + piece.rotateLeft(); + break; + case 'rotateRight': + piece.rotateRight(); + break; + default: + nope(action.type); + throw new Error(`invalid action "${action.type}"`); + } + } + start() { + this.pausedAt = null; + switch (this.state) { + case 'running': + // no-op + break; + case 'paused': + case 'not-started': + this.lastTick = new Date(); + this.lastUpdate = new Date(); + this.startedAt = new Date(); + this.state = 'running'; + this.update(); + break; + case 'finished': + throw new Error('Cannot start a finished game'); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + pause() { + switch (this.state) { + case 'running': + case 'paused': + this.pausedAt = new Date(); + this.state = 'paused'; + this.lastFrameTimes = []; + break; + case 'not-started': + case 'finished': + throw new Error(`Cannot pause game while it's in state "${this.state}"`); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + update() { + const tickStart = Date.now(); + this.lastFrameTimes.push(tickStart); + if (this.lastFrameTimes.length > 20) { + this.lastFrameTimes.shift(); + } + switch (this.state) { + case 'running': + this.performUpdate(); + break; + case 'paused': + case 'not-started': + case 'finished': + throw new Error(`Cannot advance game while in state "${this.state}"`); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + const elapsed = Date.now() - tickStart; + const nextTickTimeout = Math.max(0, (1000 / this.fps) - elapsed); + setTimeout(() => { + switch (this.state) { + case 'running': + this.update(); + break; + case 'paused': + case 'not-started': + case 'finished': + break; + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + }, nextTickTimeout); + } + performUpdate() { + const sinceLastUpdate = Date.now() - this.lastUpdate.getTime(); + this.msUntilNextTick -= sinceLastUpdate; + this.lastUpdate = new Date(); + if (this.msUntilNextTick <= 0) { + this.tick(); + } + // detect collision + // is every column filled? remove column + // is every row filled? end game + ui.update(); + } + moveIfValid(piece, xDelta, yDelta) { + const translation = { + xDelta, + yDelta, + }; + if (this.ui.canMovePiece(piece, translation)) { + piece.move(translation); + return true; + } + return false; + } + ; + tick() { + const inPlayPieces = this.pieces.filter(piece => piece.state === 'in-play'); + if (!inPlayPieces.length) { + const newPiece = TetrissimoPiece.T.clone(); + newPiece.state = 'in-play'; + this.pieces.push(newPiece); + this.ui.addPiece(newPiece); + } + else { + inPlayPieces.forEach((piece) => { + if (!this.moveIfValid(piece, 0, 1)) { + piece.setFixed(); + } + }); + } + this.msUntilNextTick = Math.round(1000 / this.currentLevel.movementsPerSecond); + this.lastTick = new Date(); + } +} +class TetrissimoUI { + constructor(canvas, playArea, + // nextBox: TetrissimoNextBox, + hud) { + this.pieces = []; + this.playArea = playArea; + // this.nextBox = nextBox; + this.hud = hud; + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('failed to get 2d context'); + } + this.options = { + borderWidth: 1, + canvas: canvas, + context, + gridLines: 4, + hud: { + width: 32, + height: 12, + }, + playArea: { + width: 68, + height: 128, + }, + padding: 2, + pixelSize: 4, + }; + } + setOptions(options) { + Object.keys(options).forEach((key) => { + this.options[key] = options[key]; + }); + } + addPiece(piece) { + this.pieces.push(piece); + } + getPiecePosition(piece) { + const origin = this.getPieceOrigin(piece); + const offset = piece.getOffset(); + return { + x: origin.x + offset.x, + y: origin.y + offset.y, + }; + } + canMovePiece(piece, delta) { + // check each block in layout's edge to see if it's in a valid area?s + const layout = piece.getLayout(); + const position = this.getPiecePosition(piece); + for (let i = 0; i < layout.length; i++) { + const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength) + (delta.yDelta * TetrissimoPiece.blockLength); + for (let j = 0; j < layout[i].length; j++) { + if (layout[i][j] === 0) { + continue; + } + const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength) + (delta.xDelta * TetrissimoPiece.blockLength); + const isCollision = this.hasCollision({ + x: blockLeftEdge, + y: blockTopEdge, + width: TetrissimoPiece.blockLength, + height: TetrissimoPiece.blockLength, + }); + if (isCollision) { + return false; + } + } + } + return true; + } + hasCollision(rect) { + const playAreaLeftEdge = this.options.padding; + const playAreaRightEdge = playAreaLeftEdge + this.options.playArea.width; + const playAreaTopEdge = this.options.padding; + const playAreaBottomEdge = playAreaTopEdge + this.options.playArea.height; + const leftEdge = rect.x; + const rightEdge = rect.x + rect.width; + const topEdge = rect.y; + const bottomEdge = rect.y + rect.height; + const isInPlayArea = leftEdge >= playAreaLeftEdge && + rightEdge <= playAreaRightEdge && + bottomEdge <= playAreaBottomEdge && + topEdge >= playAreaTopEdge; + if (!isInPlayArea) { + return true; + } + const fixedPieces = this.pieces.filter(piece => piece.state === 'fixed'); + for (const piece of fixedPieces) { + const position = this.getPiecePosition(piece); + const layout = piece.getLayout(); + for (let i = 0; i < layout.length; i++) { + const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength); + const blockBottomEdge = blockTopEdge + TetrissimoPiece.blockLength; + for (let j = 0; j < layout[i].length; j++) { + if (layout[i][j] === 0) { + continue; + } + const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength); + const blockRightEdge = blockLeftEdge + TetrissimoPiece.blockLength; + const isCollision = leftEdge >= blockLeftEdge && + rightEdge <= blockRightEdge && + bottomEdge <= blockBottomEdge && + topEdge >= blockTopEdge; + if (isCollision) { + return true; + } + } + } + } + return false; + } + update() { + const options = this.options; + const context = this.options.context; + context.clearRect(0, 0, canvas.width, canvas.height); + // fill background + context.fillStyle = 'rgb(224,158,107)'; + context.fillRect(0, 0, canvas.width, canvas.height); + // draw some grid lines + if (typeof (options.gridLines) === 'number') { + context.strokeStyle = 'rgba(255, 255, 255, 0.25)'; + context.lineWidth = 1; + const increment = options.pixelSize * options.gridLines; + for (let i = 0; i < options.canvas.width; i += increment) { + // draw vertical line + context.beginPath(); + context.moveTo(i, 0); + context.lineTo(i, options.canvas.height); + context.stroke(); + } + for (let i = 0; i < options.canvas.height; i += increment) { + // draw horizontal lines + context.beginPath(); + context.moveTo(0, i); + context.lineTo(options.canvas.width, i); + context.stroke(); + } + } + const totalWidth = Math.floor(canvas.width / options.pixelSize); + const padding = options.padding; + // draw HUD in top right corner + this.hud.render(context, Object.assign(Object.assign({}, options), { x: totalWidth - padding - options.hud.width, y: padding })); + // draw play area + this.playArea.render(context, Object.assign(Object.assign({}, options), { x: padding, y: padding })); + this.pieces.forEach((piece, i) => { + piece.render(context, Object.assign(Object.assign({}, options), this.getPieceOrigin(piece))); + }); + } + getPieceOrigin(piece) { + const length = TetrissimoPiece.blockLength; + // TODO rotation will cause problems, need a "PieceDefinition" or something with a static layout + let translateX = Math.floor((piece.getBlockWidth() * length) / 2); + // we can only perform movements in multiples of the blockLength, so we must clamp it + // to ensure it's not offset + translateX -= (translateX % length); + const padding = this.options.padding; + return { + x: padding + Math.round(this.options.playArea.width / 2) - translateX, + y: padding, + }; + } +} +class TetrissimoNextBox { + render(context, options) { + } +} +class TetrissimoHUD { + render(context, options) { + const position = { + x: options.x, + y: options.y, + width: options.hud.width, + height: options.hud.height, + }; + drawBorderedRect(position, context, options); + } +} +class TetrissimoPlayArea { + render(context, options) { + const position = { + x: options.x, + y: options.y, + width: options.playArea.width, + height: options.playArea.height, + }; + drawBorderedRect(position, context, options); + } +} +class TetrissimoPiece { + constructor(name, layout, color) { + this.offset = { x: 0, y: 0 }; + this.state = 'in-play'; + this.name = name; + this.originalLayout = layout.map(row => row.concat([])); + this.layout = layout.concat([]); + this.color = color; + } + getOffset() { + return this.offset; + } + getLayout() { + return this.layout; + } + move(translation) { + if (this.state !== 'in-play') { + return; + } + this.offset.x += (translation.xDelta * TetrissimoPiece.blockLength); + this.offset.y += (translation.yDelta * TetrissimoPiece.blockLength); + } + rotateLeft() { + if (this.state !== 'in-play') { + return; + } + // clockwise turn + const newLayout = []; + for (let i = 0; i < this.layout.length; i++) { + const col = this.layout.length - 1 - i; + for (let j = 0; j < this.layout[i].length; j++) { + const row = j; + if (!newLayout[row]) { + newLayout[row] = []; + } + newLayout[row][col] = this.layout[i][j]; + } + } + this.layout = newLayout; + } + rotateRight() { + if (this.state !== 'in-play') { + return; + } + const newLayout = []; + for (let i = 0; i < this.layout.length; i++) { + const col = i; + for (let j = 0; j < this.layout[i].length; j++) { + const row = this.layout[i].length - 1 - j; + if (!newLayout[row]) { + newLayout[row] = []; + } + newLayout[row][col] = this.layout[i][j]; + } + } + this.layout = newLayout; + } + setFixed() { + this.state = 'fixed'; + } + getBlockWidth() { + return this.layout + .map(arr => arr.filter(value => value === 1).length) + .reduce((max, len) => Math.max(max, len), 0); + } + getBlockHeight() { + const cols = []; + for (let i = 0; i < this.layout.length; i++) { + for (let j = 0; j < this.layout[i].length; j++) { + if (!cols[j]) { + cols[j] = []; + } + cols[j].push(this.layout[i][j]); + } + } + return cols + .map(arr => arr.filter(value => value === 1).length) + .reduce((max, len) => Math.max(max, len), 0); + } + clone() { + return new TetrissimoPiece(this.name, this.layout, this.color); + } + render(context, options) { + this.layout.forEach((columns, rowNum) => { + columns.forEach((value, colNum) => { + switch (value) { + case 0: + break; + case 1: + context.fillStyle = this.color; + const length = TetrissimoPiece.blockLength; + const offsetX = this.offset.x; + const offsetY = this.offset.y; + const x = options.x + offsetX + (colNum * length); + const y = options.y + offsetY + (rowNum * length); + const actualLength = getSize(length, options); + context.fillRect(getSize(x, options), getSize(y, options), actualLength, actualLength); + break; + default: + nope(value); + throw new Error(`Invalid piece cell value "${value}"`); + } + }); + }); + } +} +TetrissimoPiece.blockLength = 4; +TetrissimoPiece.J = new TetrissimoPiece('J', [ + [0, 1], + [0, 1], + [1, 1], +], 'black'); +TetrissimoPiece.S = new TetrissimoPiece('S', [ + [0, 1, 1], + [1, 1, 0], +], 'green'); +TetrissimoPiece.T = new TetrissimoPiece('T', [ + [1, 1, 1], + [0, 1, 0], +], 'blue'); +TetrissimoPiece.Z = new TetrissimoPiece('Z', [ + [1, 1, 0,], + [0, 1, 1,], +], 'yellow'); +TetrissimoPiece.I = new TetrissimoPiece('I', [ + [1, 1, 1, 1], +], 'magenta'); +TetrissimoPiece.L = new TetrissimoPiece('L', [ + [1], + [1], + [1, 1], +], 'cyan'); +TetrissimoPiece.O = new TetrissimoPiece('O', [ + [1, 1], + [1, 1], +], 'red'); +// --------------------------------------------- +const hud = new TetrissimoHUD(); +const playArea = new TetrissimoPlayArea(); +const canvas = document.getElementsByTagName('canvas')[0]; +if (!canvas) { + throw new Error('no canvas'); +} +const ui = new TetrissimoUI(canvas, playArea, hud); +const game = new TetrissimoGame(ui); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGV0cmlzc2ltby5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInRldHJpc3NpbW8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUEsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFRLEVBQVEsRUFBRSxHQUFFLENBQUMsQ0FBQztBQXFEcEMsTUFBTSxPQUFPLEdBQUcsQ0FBQyxLQUFhLEVBQUUsT0FBMkMsRUFBVSxFQUFFLENBQUMsS0FBSyxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUM7QUFFbEgsTUFBTSxnQkFBZ0IsR0FBRyxDQUN4QixRQUE0QyxFQUM1QyxPQUFpQyxFQUNqQyxPQUErQixFQUN4QixFQUFFO0lBQ1QsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLFdBQVcsQ0FBQztJQUN4QyxJQUFJLENBQUMsV0FBVyxFQUFFO1FBQ2pCLE9BQU87S0FDUDtJQUVELE1BQU0sZUFBZSxHQUFHLE9BQU8sQ0FBQyxXQUFXLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDdEQsT0FBTyxDQUFDLFNBQVMsR0FBRyxlQUFlLENBQUM7SUFDcEMsT0FBTyxDQUFDLFdBQVcsR0FBRyxPQUFPLENBQUM7SUFDOUIsT0FBTyxDQUFDLFVBQVUsQ0FDakIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsT0FBTyxDQUFDLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxlQUFlLEdBQUcsQ0FBQyxDQUFDLEVBQzlELE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsZUFBZSxHQUFHLENBQUMsQ0FBQyxFQUM5RCxPQUFPLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxPQUFPLENBQUMsR0FBRyxlQUFlLEVBQ2xELE9BQU8sQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxHQUFHLGVBQWUsQ0FDbkQsQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLE1BQU0sZUFBZTtJQWVwQixZQUFtQixLQUFhLEVB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LEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO1lBQzVDLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDdkMsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO2dCQUMvQyxNQUFNLEdBQUcsR0FBRyxDQUFDLENBQUM7Z0JBQ2QsSUFBSSxDQUFDLFNBQVMsQ0FBQyxHQUFHLENBQUMsRUFBRTtvQkFDcEIsU0FBUyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQztpQkFDcEI7Z0JBQ0QsU0FBUyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7YUFDeEM7U0FDRDtRQUVELElBQUksQ0FBQyxNQUFNLEdBQUcsU0FBUyxDQUFDO0lBQ3pCLENBQUM7SUFFTSxXQUFXO1FBQ2pCLElBQUksSUFBSSxDQUFDLEtBQUssS0FBSyxTQUFTLEVBQUU7WUFDN0IsT0FBTztTQUNQO1FBRUQsTUFBTSxTQUFTLEdBQWdCLEVBQUUsQ0FBQztRQUNsQyxLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQyxFQUFFLEVBQUU7WUFDNUMsTUFBTSxHQUFHLEdBQUcsQ0FBQyxDQUFDO1lBQ2QsS0FBSyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsRUFBRSxFQUFFO2dCQUMvQyxNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0s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QU0sRUFBRSxRQUFRLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDbkQsTUFBTSxJQUFJLEdBQUcsSUFBSSxjQUFjLENBQUMsRUFBRSxDQUFDLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJ0eXBlIEdhbWVTdGF0ZSA9ICdydW5uaW5nJyB8ICdwYXVzZWQnIHwgJ25vdC1zdGFydGVkJyB8ICdmaW5pc2hlZCc7XG5cbmNvbnN0IG5vcGUgPSAoeDogbmV2ZXIpOiB2b2lkID0+IHt9O1xuXG50eXBlIFBpZWNlQ2VsbCA9IDAgfCAxO1xudHlwZSBQaWVjZUxheW91dCA9IFBpZWNlQ2VsbFtdW107XG5cbmludGVyZmFjZSBVSUVsZW1lbnQge1xuXHRyZW5kZXIoY29udGV4dDogQ2FudmFzUmVuZGVyaW5nQ29udGV4dDJELCBvcHRpb25zOiBVSUVsZW1lbnRSZW5kZXJPcHRpb25zKTogdm9pZDtcbn1cblxuaW50ZXJmYWNlIERpbWVuc2lvbnMge1xuXHR3aWR0aDogbnVtYmVyO1xuXHRoZWlnaHQ6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIEdsb2JhbFVJT3B0aW9ucyB7XG5cdHBpeGVsU2l6ZTogbnVtYmVyO1xuXHRwYWRkaW5nOiBudW1iZXI7XG5cdGNhbnZhczogSFRNTENhbnZhc0VsZW1lbnQ7XG5cdGNvbnRleHQ6IENhbnZhc1JlbmRlcmluZ0NvbnRleHQyRDtcblx0Z3JpZExpbmVzOiBmYWxzZSB8IG51bWJlcjtcblx0Ym9yZGVyV2lkdGg6IG51bWJlcjtcblx0aHVkOiBEaW1lbnNpb25zO1xuXHRwbGF5QXJlYTogRGltZW5zaW9ucztcbn1cblxuaW50ZXJmYWNlIFVJRWxlbWVudFJlbmRlck9wdGlvbnMgZXh0ZW5kcyBHbG9iYWxVSU9wdGlvbnMge1xuXHR4OiBudW1iZXI7XG5cdHk6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIFBpZWNlVHJhbnNsYXRpb24ge1xuXHR4RGVsdGE6IG51bWJlcjtcblx0eURlbHRhOiBudW1iZXI7XG59XG5cbmludGVyZmFjZSBUZXRyaXNzaW1vQ29vcmRpbmF0ZXMge1xuXHR4OiBudW1iZXI7XG5cdHk6IG51bWJlcjtcbn1cblxuaW50ZXJmYWNlIFJlY3RhbmdsZUNvb3JkaW5hdGVzIGV4dGVuZHMgVGV0cmlzc2ltb0Nvb3JkaW5hdGVzLCBEaW1lbnNpb25zIHt9XG5cbnR5cGUgUGllY2VBY3Rpb25UeXBlID1cblx0J21vdmVMZWZ0JyB8XG5cdCdtb3ZlUmlnaHQnIHxcblx0J21vdmVEb3duJyB8XG5cdCdyb3RhdGVMZWZ0JyB8XG5cdCdyb3RhdGVSaWdodCc7XG5cbmludGVyZmFjZSBQaWVjZUFjdGlvbiB7XG5cdHR5cGU6IFBpZWNlQWN0aW9uVHlwZTtcbn1cblxuY29uc3QgZ2V0U2l6ZSA9ICh2YWx1ZTogbnVtYmVyLCBvcHRpb25zOiBQaWNrPEdsb2JhbFVJT3B0aW9ucywgJ3BpeGVsU2l6ZSc+KTogbnVtYmVyID0+IHZhbHVlICogb3B0aW9ucy5waXhlbFNpemU7XG5cbmNvbnN0IGRyYXdCb3JkZXJlZFJlY3QgPSAoXG5cdHBvc2l0aW9uOiBEaW1lbnNpb25zICYgVGV0cmlzc2ltb0Nvb3JkaW5hdGVzLFxuXHRjb250ZXh0OiBDYW52YXNSZW5kZXJpbmdDb250ZXh0MkQsXG5cdG9wdGlvbnM6IFVJRWxlbWVudFJlbmRlck9wdGlvbnMsXG4pOiB2b2lkID0+IHtcblx0Y29uc3QgYm9yZGVyV2lkdGggPSBvcHRpb25zLmJvcmRlcldpZHRoO1xuXHRpZiAoIWJvcmRlcldpZHRoKSB7XG5cdFx0cmV0dXJuO1xuXHR9XG5cblx0Y29uc3QgYWN0dWFsTGluZVdpZHRoID0gZ2V0U2l6ZShib3JkZXJXaWR0aCwgb3B0aW9ucyk7XG5cdGNvbnRleHQubGluZVdpZHRoID0gYWN0dWFsTGluZVdpZHRoO1xuXHRjb250ZXh0LnN0cm9rZVN0eWxlID0gJ3doaXRlJztcblx0Y29udGV4dC5zdHJva2VSZWN0KFxuXHRcdGdldFNpemUocG9zaXRpb24ueCwgb3B0aW9ucykgLSBNYXRoLnJvdW5kKGFjdHVhbExpbmVXaWR0aCAvIDIpLFxuXHRcdGdldFNpemUocG9zaXRpb24ueSwgb3B0aW9ucykgLSBNYXRoLnJvdW5kKGFjdHVhbExpbmVXaWR0aCAvIDIpLFxuXHRcdGdldFNpemUocG9zaXRpb24ud2lkdGgsIG9wdGlvbnMpICsgYWN0dWFsTGluZVdpZHRoLFxuXHRcdGdldFNpemUocG9zaXRpb24uaGVpZ2h0LCBvcHRpb25zKSArIGFjdHVhbExpbmVXaWR0aCxcblx0KTtcbn07XG5cbmNsYXNzIFRldHJpc3NpbW9MZXZlbCB7XG5cdHB1YmxpYyByZWFkb25seSBvcmRlcjogbnVtYmVyO1xuXHRwdWJsaWMgcmVhZG9ubHkgbW92ZW1lbnRzUGVyU2Vjb25kOiBudW1iZXI7XG5cblx0cHVibGljIHN0YXRpYyByZWFkb25seSBsZXZlbDEgPSBuZXcgVGV0cmlzc2ltb0xldmVsKDEsIDAuNSk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgbGV2ZWwyID0gbmV3IFRldHJpc3NpbW9MZXZlbCgyLCAwLjc1KTtcblx0cHVibGljIHN0YXRpYyByZWFkb25seSBsZXZlbDMgPSBuZXcgVGV0cmlzc2ltb0xldmVsKDIsIDEpO1xuXHRwdWJsaWMgc3RhdGljIHJlYWRvbmx5IGxldmVsNCA9IG5ldyBUZXRyaXNzaW1vTGV2ZWwoMiwgMS41KTtcblx0cHVibGljIHN0YXRpYyByZWFkb25seSBsZXZlbDUgPSBuZXcgVGV0cmlzc2ltb0xldmVsKDIsIDIpO1xuXHRwdWJsaWMgc3RhdGljIHJlYWRvbmx5IGxldmVsNiA9IG5ldyBUZXRyaXNzaW1vTGV2ZWwoMiwgMyk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgbGV2ZWw3ID0gbmV3IFRldHJpc3NpbW9MZXZlbCgyLCA0KTtcblx0cHVibGljIHN0YXRpYyByZWFkb25seSBsZXZlbDggPSBuZXcgVGV0cmlzc2ltb0xldmVsKDIsIDYpO1xuXHRwdWJsaWMgc3RhdGljIHJlYWRvbmx5IGxldmVsOSA9IG5ldyBUZXRyaXNzaW1vTGV2ZWwoMiwgOCk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgbGV2ZWwxMCA9IG5ldyBUZXRyaXNzaW1vTGV2ZWwoMiwgMTApO1xuXG5cdHB1YmxpYyBjb25zdHJ1Y3RvcihvcmRlcjogbnVtYmVyLCBzcGVlZDogbnVtYmVyKSB7XG5cdFx0dGhpcy5vcmRlciA9IG9yZGVyO1xuXHRcdHRoaXMubW92ZW1lbnRzUGVyU2Vjb25kID0gc3BlZWQ7XG5cdH1cbn1cblxuY2xhc3MgVGV0cmlzc2ltb0dhbWUge1xuXHRwcml2YXRlIHN0YXJ0ZWRBdDogRGF0ZSB8IG51bGwgPSBudWxsO1xuXHRwcml2YXRlIHBhdXNlZEF0OiBEYXRlIHwgbnVsbCA9IG51bGw7XG5cdHByaXZhdGUgZmluaXNoZWRBdDogRGF0ZSB8IG51bGwgPSBudWxsO1xuXHRwcml2YXRlIHN0YXRlOiBHYW1lU3RhdGUgPSAnbm90LXN0YXJ0ZWQnO1xuXHRwcml2YXRlIGZwcyA9IDYwO1xuXHRwcml2YXRlIHJlYWRvbmx5IHVpOiBUZXRyaXNzaW1vVUk7XG5cdHByaXZhdGUgbGFzdEZyYW1lVGltZXM6IG51bWJlcltdID0gW107XG5cdHByaXZhdGUgcmVhZG9ubHkgcGllY2VzOiBUZXRyaXNzaW1vUGllY2VbXSA9IFtdO1xuXHRwdWJsaWMgY3VycmVudExldmVsOiBUZXRyaXNzaW1vTGV2ZWwgPSBUZXRyaXNzaW1vTGV2ZWwubGV2ZWwxO1xuXHRwcml2YXRlIG1zVW50aWxOZXh0VGljayA9IDA7XG5cdHByaXZhdGUgbGFzdFRpY2sgPSBuZXcgRGF0ZSgpO1xuXHRwcml2YXRlIGxhc3RVcGRhdGUgPSBuZXcgRGF0ZSgpO1xuXG5cdHB1YmxpYyBjb25zdHJ1Y3Rvcih1aTogVGV0cmlzc2ltb1VJKSB7XG5cdFx0dGhpcy51aSA9IHVpO1xuXHR9XG5cblx0cHVibGljIGdldFN0YXRlKCk6IEdhbWVTdGF0ZSB7XG5cdFx0cmV0dXJuIHRoaXMuc3RhdGU7XG5cdH1cblxuXHRwdWJsaWMgc2V0VGFyZ2V0RnBzKGZwczogbnVtYmVyKSB7XG5cdFx0dGhpcy5mcHMgPSBmcHM7XG5cdH1cblxuXHRwdWJsaWMgc2V0TGV2ZWwobGV2ZWw6IFRldHJpc3NpbW9MZXZlbCkge1xuXHRcdHRoaXMuY3VycmVudExldmVsID0gbGV2ZWw7XG5cdH1cblxuXHRwdWJsaWMgZ2V0Q3VycmVudEZwcygpOiBudW1iZXIge1xuXHRcdHN3aXRjaCAodGhpcy5zdGF0ZSkge1xuXHRcdFx0Y2FzZSAncnVubmluZyc6XG5cdFx0XHRcdGlmICh0aGlzLmxhc3RGcmFtZVRpbWVzLmxlbmd0aCA8PSAxKSB7XG5cdFx0XHRcdFx0cmV0dXJuIDA7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRjb25zdCBmaXJzdEZyYW1lID0gdGhpcy5sYXN0RnJhbWVUaW1lc1swXTtcblx0XHRcdFx0Y29uc3QgbGFzdEZyYW1lID0gdGhpcy5sYXN0RnJhbWVUaW1lc1t0aGlzLmxhc3RGcmFtZVRpbWVzLmxlbmd0aCAtIDFdO1xuXHRcdFx0XHRjb25zdCBlbGFwc2VkTXMgPSBsYXN0RnJhbWUgLSBmaXJzdEZyYW1lO1xuXHRcdFx0XHRjb25zdCBhdmdNc1BlckZyYW1lID0gKGVsYXBzZWRNcyAvIHRoaXMubGFzdEZyYW1lVGltZXMubGVuZ3RoKTtcblx0XHRcdFx0cmV0dXJuIDEwMDAgLyBhdmdNc1BlckZyYW1lO1xuXHRcdFx0Y2FzZSAncGF1c2VkJzpcblx0XHRcdGNhc2UgJ25vdC1zdGFydGVkJzpcblx0XHRcdGNhc2UgJ2ZpbmlzaGVkJzpcblx0XHRcdFx0cmV0dXJuIDA7XG5cdFx0XHRkZWZhdWx0OlxuXHRcdFx0XHRub3BlKHRoaXMuc3RhdGUpO1xuXHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgZ2FtZSBzdGF0ZSBcIiR7dGhpcy5zdGF0ZX1cImApO1xuXHRcdH1cblx0fVxuXG5cdHB1YmxpYyBwZXJmb3JtUGllY2VBY3Rpb24oYWN0aW9uOiBQaWVjZUFjdGlvbikge1xuXHRcdGlmICh0aGlzLnN0YXRlICE9PSAncnVubmluZycpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cblx0XHRjb25zdCBwaWVjZSA9IHRoaXMucGllY2VzLmZpbHRlcihwaWVjZSA9PiBwaWVjZS5zdGF0ZSA9PT0gJ2luLXBsYXknKVswXTtcblx0XHRpZiAoIXBpZWNlKSB7XG5cdFx0XHQvLyBub3RoaW5nIHRvIGRvLCBubyBwaWVjZXMgaW4gcGxheVxuXHRcdFx0cmV0dXJuO1xuXHRcdH1cblxuXHRcdGNvbnN0IG1vdmVtZW50SW5jcmVtZW50ID0gMTtcblxuXHRcdHN3aXRjaCAoYWN0aW9uLnR5cGUpIHtcblx0XHRcdGNhc2UgJ21vdmVMZWZ0Jzpcblx0XHRcdFx0dGhpcy5tb3ZlSWZWYWxpZChwaWVjZSwgLW1vdmVtZW50SW5jcmVtZW50LCAwKTtcblx0XHRcdFx0YnJlYWs7XG5cdFx0XHRjYXNlICdtb3ZlUmlnaHQnOlxuXHRcdFx0XHR0aGlzLm1vdmVJZlZhbGlkKHBpZWNlLCBtb3ZlbWVudEluY3JlbWVudCwgMCk7XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0Y2FzZSAnbW92ZURvd24nOlxuXHRcdFx0XHRpZiAoIXRoaXMubW92ZUlmVmFsaWQocGllY2UsIDAsIG1vdmVtZW50SW5jcmVtZW50KSkge1xuXHRcdFx0XHRcdC8vIG1ha2UgZml4ZWQgdG8gYm90dG9tIG9mIHBsYXkgYXJlYVxuXHRcdFx0XHRcdHBpZWNlLnNldEZpeGVkKCk7XG5cdFx0XHRcdH1cblx0XHRcdFx0YnJlYWs7XG5cdFx0XHRjYXNlICdyb3RhdGVMZWZ0Jzpcblx0XHRcdFx0Ly8gVE9ETyBlbnN1cmUgaXQncyBhIHZhbGlkIG1vdmVcblx0XHRcdFx0cGllY2Uucm90YXRlTGVmdCgpO1xuXHRcdFx0XHRicmVhaztcblx0XHRcdGNhc2UgJ3JvdGF0ZVJpZ2h0Jzpcblx0XHRcdFx0cGllY2Uucm90YXRlUmlnaHQoKTtcblx0XHRcdFx0YnJlYWs7XG5cdFx0XHRkZWZhdWx0OlxuXHRcdFx0XHRub3BlKGFjdGlvbi50eXBlKTtcblx0XHRcdFx0dGhyb3cgbmV3IEVycm9yKGBpbnZhbGlkIGFjdGlvbiBcIiR7YWN0aW9uLnR5cGV9XCJgKTtcblx0XHR9XG5cdH1cblxuXHRwdWJsaWMgc3RhcnQoKSB7XG5cdFx0dGhpcy5wYXVzZWRBdCA9IG51bGw7XG5cblx0XHRzd2l0Y2ggKHRoaXMuc3RhdGUpIHtcblx0XHRcdGNhc2UgJ3J1bm5pbmcnOlxuXHRcdFx0XHQvLyBuby1vcFxuXHRcdFx0XHRicmVhaztcblx0XHRcdGNhc2UgJ3BhdXNlZCc6XG5cdFx0XHRjYXNlICdub3Qtc3RhcnRlZCc6XG5cdFx0XHRcdHRoaXMubGFzdFRpY2sgPSBuZXcgRGF0ZSgpO1xuXHRcdFx0XHR0aGlzLmxhc3RVcGRhdGUgPSBuZXcgRGF0ZSgpO1xuXHRcdFx0XHR0aGlzLnN0YXJ0ZWRBdCA9IG5ldyBEYXRlKCk7XG5cdFx0XHRcdHRoaXMuc3RhdGUgPSAncnVubmluZyc7XG5cdFx0XHRcdHRoaXMudXBkYXRlKCk7XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0Y2FzZSAnZmluaXNoZWQnOlxuXHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoJ0Nhbm5vdCBzdGFydCBhIGZpbmlzaGVkIGdhbWUnKTtcblx0XHRcdGRlZmF1bHQ6XG5cdFx0XHRcdG5vcGUodGhpcy5zdGF0ZSk7XG5cdFx0XHRcdHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBnYW1lIHN0YXRlIFwiJHt0aGlzLnN0YXRlfVwiYCk7XG5cdFx0fVxuXHR9XG5cblx0cHVibGljIHBhdXNlKCkge1xuXHRcdHN3aXRjaCAodGhpcy5zdGF0ZSkge1xuXHRcdFx0Y2FzZSAncnVubmluZyc6XG5cdFx0XHRjYXNlICdwYXVzZWQnOlxuXHRcdFx0XHR0aGlzLnBhdXNlZEF0ID0gbmV3IERhdGUoKTtcblx0XHRcdFx0dGhpcy5zdGF0ZSA9ICdwYXVzZWQnO1xuXHRcdFx0XHR0aGlzLmxhc3RGcmFtZVRpbWVzID0gW107XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0Y2FzZSAnbm90LXN0YXJ0ZWQnOlxuXHRcdFx0Y2FzZSAnZmluaXNoZWQnOlxuXHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoYENhbm5vdCBwYXVzZSBnYW1lIHdoaWxlIGl0J3MgaW4gc3RhdGUgXCIke3RoaXMuc3RhdGV9XCJgKTtcblx0XHRcdGRlZmF1bHQ6XG5cdFx0XHRcdG5vcGUodGhpcy5zdGF0ZSk7XG5cdFx0XHRcdHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBnYW1lIHN0YXRlIFwiJHt0aGlzLnN0YXRlfVwiYCk7XG5cdFx0fVxuXHR9XG5cblx0cHVibGljIHVwZGF0ZSgpOiB2b2lkIHtcblx0XHRjb25zdCB0aWNrU3RhcnQgPSBEYXRlLm5vdygpO1xuXHRcdHRoaXMubGFzdEZyYW1lVGltZXMucHVzaCh0aWNrU3RhcnQpO1xuXHRcdGlmICh0aGlzLmxhc3RGcmFtZVRpbWVzLmxlbmd0aCA+IDIwKSB7XG5cdFx0XHR0aGlzLmxhc3RGcmFtZVRpbWVzLnNoaWZ0KCk7XG5cdFx0fVxuXG5cdFx0c3dpdGNoICh0aGlzLnN0YXRlKSB7XG5cdFx0XHRjYXNlICdydW5uaW5nJzpcblx0XHRcdFx0dGhpcy5wZXJmb3JtVXBkYXRlKCk7XG5cdFx0XHRcdGJyZWFrO1xuXHRcdFx0Y2FzZSAncGF1c2VkJzpcblx0XHRcdGNhc2UgJ25vdC1zdGFydGVkJzpcblx0XHRcdGNhc2UgJ2ZpbmlzaGVkJzpcblx0XHRcdFx0dGhyb3cgbmV3IEVycm9yKGBDYW5ub3QgYWR2YW5jZSBnYW1lIHdoaWxlIGluIHN0YXRlIFwiJHt0aGlzLnN0YXRlfVwiYCk7XG5cdFx0XHRkZWZhdWx0OlxuXHRcdFx0XHRub3BlKHRoaXMuc3RhdGUpO1xuXHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgZ2FtZSBzdGF0ZSBcIiR7dGhpcy5zdGF0ZX1cImApO1xuXHRcdH1cblxuXHRcdGNvbnN0IGVsYXBzZWQgPSBEYXRlLm5vdygpIC0gdGlja1N0YXJ0O1xuXHRcdGNvbnN0IG5leHRUaWNrVGltZW91dCA9IE1hdGgubWF4KDAsICgxMDAwIC8gdGhpcy5mcHMpIC0gZWxhcHNlZCk7XG5cdFx0c2V0VGltZW91dCgoKSA9PiB7XG5cdFx0XHRzd2l0Y2ggKHRoaXMuc3RhdGUpIHtcblx0XHRcdFx0Y2FzZSAncnVubmluZyc6XG5cdFx0XHRcdFx0dGhpcy51cGRhdGUoKTtcblx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0Y2FzZSAncGF1c2VkJzpcblx0XHRcdFx0Y2FzZSAnbm90LXN0YXJ0ZWQnOlxuXHRcdFx0XHRjYXNlICdmaW5pc2hlZCc6XG5cdFx0XHRcdFx0YnJlYWs7XG5cdFx0XHRcdGRlZmF1bHQ6XG5cdFx0XHRcdFx0bm9wZSh0aGlzLnN0YXRlKTtcblx0XHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgZ2FtZSBzdGF0ZSBcIiR7dGhpcy5zdGF0ZX1cImApO1xuXHRcdFx0fVxuXHRcdH0sIG5leHRUaWNrVGltZW91dCk7XG5cdH1cblxuXHRwcml2YXRlIHBlcmZvcm1VcGRhdGUoKTogdm9pZCB7XG5cdFx0Y29uc3Qgc2luY2VMYXN0VXBkYXRlID0gRGF0ZS5ub3coKSAtIHRoaXMubGFzdFVwZGF0ZS5nZXRUaW1lKCk7XG5cdFx0dGhpcy5tc1VudGlsTmV4dFRpY2sgLT0gc2luY2VMYXN0VXBkYXRlO1xuXHRcdHRoaXMubGFzdFVwZGF0ZSA9IG5ldyBEYXRlKCk7XG5cblx0XHRpZiAodGhpcy5tc1VudGlsTmV4dFRpY2sgPD0gMCkge1xuXHRcdFx0dGhpcy50aWNrKCk7XG5cdFx0fVxuXG5cdFx0Ly8gZGV0ZWN0IGNvbGxpc2lvblxuXHRcdC8vIGlzIGV2ZXJ5IGNvbHVtbiBmaWxsZWQ/IHJlbW92ZSBjb2x1bW5cblx0XHQvLyBpcyBldmVyeSByb3cgZmlsbGVkPyBlbmQgZ2FtZVxuXG5cdFx0dWkudXBkYXRlKCk7XG5cdH1cblxuXHRwcml2YXRlIG1vdmVJZlZhbGlkKHBpZWNlOiBUZXRyaXNzaW1vUGllY2UsIHhEZWx0YTogbnVtYmVyLCB5RGVsdGE6IG51bWJlcik6IGJvb2xlYW4ge1xuXHRcdGNvbnN0IHRyYW5zbGF0aW9uOiBQaWVjZVRyYW5zbGF0aW9uID0ge1xuXHRcdFx0eERlbHRhLFxuXHRcdFx0eURlbHRhLFxuXHRcdH07XG5cdFx0aWYgKHRoaXMudWkuY2FuTW92ZVBpZWNlKHBpZWNlLCB0cmFuc2xhdGlvbikpIHtcblx0XHRcdHBpZWNlLm1vdmUodHJhbnNsYXRpb24pO1xuXHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0fVxuXG5cdFx0cmV0dXJuIGZhbHNlO1xuXHR9O1xuXG5cdHByaXZhdGUgdGljaygpOiB2b2lkIHtcblx0XHRjb25zdCBpblBsYXlQaWVjZXMgPSB0aGlzLnBpZWNlcy5maWx0ZXIocGllY2UgPT4gcGllY2Uuc3RhdGUgPT09ICdpbi1wbGF5Jyk7XG5cdFx0aWYgKCFpblBsYXlQaWVjZXMubGVuZ3RoKSB7XG5cdFx0XHRjb25zdCBuZXdQaWVjZSA9IFRldHJpc3NpbW9QaWVjZS5ULmNsb25lKCk7XG5cdFx0XHRuZXdQaWVjZS5zdGF0ZSA9ICdpbi1wbGF5Jztcblx0XHRcdHRoaXMucGllY2VzLnB1c2gobmV3UGllY2UpO1xuXHRcdFx0dGhpcy51aS5hZGRQaWVjZShuZXdQaWVjZSk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdGluUGxheVBpZWNlcy5mb3JFYWNoKChwaWVjZSkgPT4ge1xuXHRcdFx0XHRpZiAoIXRoaXMubW92ZUlmVmFsaWQocGllY2UsIDAsIDEpKSB7XG5cdFx0XHRcdFx0cGllY2Uuc2V0Rml4ZWQoKTtcblx0XHRcdFx0fVxuXHRcdFx0fSk7XG5cdFx0fVxuXG5cdFx0dGhpcy5tc1VudGlsTmV4dFRpY2sgPSBNYXRoLnJvdW5kKDEwMDAgLyB0aGlzLmN1cnJlbnRMZXZlbC5tb3ZlbWVudHNQZXJTZWNvbmQpO1xuXHRcdHRoaXMubGFzdFRpY2sgPSBuZXcgRGF0ZSgpO1xuXHR9XG59XG5cbmNsYXNzIFRldHJpc3NpbW9VSSB7XG5cdHB1YmxpYyByZWFkb25seSBwbGF5QXJlYTogVGV0cmlzc2ltb1BsYXlBcmVhO1xuXHRwcml2YXRlIHJlYWRvbmx5IHBpZWNlczogQXJyYXk8UmVhZG9ubHk8VGV0cmlzc2ltb1BpZWNlPj4gPSBbXTtcblx0cHVibGljIHJlYWRvbmx5IG5leHRCb3g6IFRldHJpc3NpbW9OZXh0Qm94O1xuXHRwdWJsaWMgcmVhZG9ubHkgaHVkOiBUZXRyaXNzaW1vSFVEO1xuXHRwdWJsaWMgcmVhZG9ubHkgb3B0aW9uczogR2xvYmFsVUlPcHRpb25zO1xuXG5cdHB1YmxpYyBjb25zdHJ1Y3Rvcihcblx0XHRjYW52YXM6IEhUTUxDYW52YXNFbGVtZW50LFxuXHRcdHBsYXlBcmVhOiBUZXRyaXNzaW1vUGxheUFyZWEsXG5cdFx0Ly8gbmV4dEJveDogVGV0cmlzc2ltb05leHRCb3gsXG5cdFx0aHVkOiBUZXRyaXNzaW1vSFVELFxuXHQpIHtcblx0XHR0aGlzLnBsYXlBcmVhID0gcGxheUFyZWE7XG5cdFx0Ly8gdGhpcy5uZXh0Qm94ID0gbmV4dEJveDtcblx0XHR0aGlzLmh1ZCA9IGh1ZDtcblxuXHRcdGNvbnN0IGNvbnRleHQgPSBjYW52YXMuZ2V0Q29udGV4dCgnMmQnKTtcblx0XHRpZiAoIWNvbnRleHQpIHtcblx0XHRcdHRocm93IG5ldyBFcnJvcignZmFpbGVkIHRvIGdldCAyZCBjb250ZXh0Jyk7XG5cdFx0fVxuXHRcdHRoaXMub3B0aW9ucyA9IHtcblx0XHRcdGJvcmRlcldpZHRoOiAxLFxuXHRcdFx0Y2FudmFzOiBjYW52YXMsXG5cdFx0XHRjb250ZXh0LFxuXHRcdFx0Z3JpZExpbmVzOiA0LFxuXHRcdFx0aHVkOiB7XG5cdFx0XHRcdHdpZHRoOiAzMixcblx0XHRcdFx0aGVpZ2h0OiAxMixcblx0XHRcdH0sXG5cdFx0XHRwbGF5QXJlYToge1xuXHRcdFx0XHR3aWR0aDogNjgsXG5cdFx0XHRcdGhlaWdodDogMTI4LFxuXHRcdFx0fSxcblx0XHRcdHBhZGRpbmc6IDIsXG5cdFx0XHRwaXhlbFNpemU6IDQsXG5cdFx0fTtcblx0fVxuXG5cdHB1YmxpYyBzZXRPcHRpb25zKG9wdGlvbnM6IE9taXQ8R2xvYmFsVUlPcHRpb25zLCAnY2FudmFzJyB8ICdjb250ZXh0Jz4pIHtcblx0XHRPYmplY3Qua2V5cyhvcHRpb25zKS5mb3JFYWNoKChrZXkpID0+IHtcblx0XHRcdHRoaXMub3B0aW9uc1trZXldID0gb3B0aW9uc1trZXldO1xuXHRcdH0pO1xuXHR9XG5cblx0cHVibGljIGFkZFBpZWNlKHBpZWNlOiBUZXRyaXNzaW1vUGllY2UpIHtcblx0XHR0aGlzLnBpZWNlcy5wdXNoKHBpZWNlKTtcblx0fVxuXG5cdHByaXZhdGUgZ2V0UGllY2VQb3NpdGlvbihwaWVjZTogUmVhZG9ubHk8VGV0cmlzc2ltb1BpZWNlPik6IFRldHJpc3NpbW9Db29yZGluYXRlcyB7XG5cdFx0Y29uc3Qgb3JpZ2luID0gdGhpcy5nZXRQaWVjZU9yaWdpbihwaWVjZSk7XG5cdFx0Y29uc3Qgb2Zmc2V0ID0gcGllY2UuZ2V0T2Zmc2V0KCk7XG5cblx0XHRyZXR1cm4ge1xuXHRcdFx0eDogb3JpZ2luLnggKyBvZmZzZXQueCxcblx0XHRcdHk6IG9yaWdpbi55ICsgb2Zmc2V0LnksXG5cdFx0fTtcblx0fVxuXG5cdHB1YmxpYyBjYW5Nb3ZlUGllY2UocGllY2U6IFJlYWRvbmx5PFRldHJpc3NpbW9QaWVjZT4sIGRlbHRhOiBQaWVjZVRyYW5zbGF0aW9uKTogYm9vbGVhbiB7XG5cdFx0Ly8gY2hlY2sgZWFjaCBibG9jayBpbiBsYXlvdXQncyBlZGdlIHRvIHNlZSBpZiBpdCdzIGluIGEgdmFsaWQgYXJlYT9zXG5cblx0XHRjb25zdCBsYXlvdXQgPSBwaWVjZS5nZXRMYXlvdXQoKTtcblx0XHRjb25zdCBwb3NpdGlvbiA9IHRoaXMuZ2V0UGllY2VQb3NpdGlvbihwaWVjZSk7XG5cblx0XHRmb3IgKGxldCBpID0gMDsgaSA8IGxheW91dC5sZW5ndGg7IGkrKykge1xuXHRcdFx0Y29uc3QgYmxvY2tUb3BFZGdlID0gcG9zaXRpb24ueSArIChpICogVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoKSArIChkZWx0YS55RGVsdGEgKiBUZXRyaXNzaW1vUGllY2UuYmxvY2tMZW5ndGgpO1xuXHRcdFx0Zm9yIChsZXQgaiA9IDA7IGogPCBsYXlvdXRbaV0ubGVuZ3RoOyBqKyspIHtcblx0XHRcdFx0aWYgKGxheW91dFtpXVtqXSA9PT0gMCkge1xuXHRcdFx0XHRcdGNvbnRpbnVlO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0Y29uc3QgYmxvY2tMZWZ0RWRnZSA9IHBvc2l0aW9uLnggKyAoaiAqIFRldHJpc3NpbW9QaWVjZS5ibG9ja0xlbmd0aCkgKyAoZGVsdGEueERlbHRhICogVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoKTtcblxuXHRcdFx0XHRjb25zdCBpc0NvbGxpc2lvbiA9IHRoaXMuaGFzQ29sbGlzaW9uKHtcblx0XHRcdFx0XHR4OiBibG9ja0xlZnRFZGdlLFxuXHRcdFx0XHRcdHk6IGJsb2NrVG9wRWRnZSxcblx0XHRcdFx0XHR3aWR0aDogVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoLFxuXHRcdFx0XHRcdGhlaWdodDogVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoLFxuXHRcdFx0XHR9KTtcblxuXHRcdFx0XHRpZiAoaXNDb2xsaXNpb24pIHtcblx0XHRcdFx0XHRyZXR1cm4gZmFsc2U7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHR9XG5cblx0XHRyZXR1cm4gdHJ1ZTtcblx0fVxuXG5cdHByaXZhdGUgaGFzQ29sbGlzaW9uKHJlY3Q6IFJlY3RhbmdsZUNvb3JkaW5hdGVzKTogYm9vbGVhbiB7XG5cdFx0Y29uc3QgcGxheUFyZWFMZWZ0RWRnZSA9IHRoaXMub3B0aW9ucy5wYWRkaW5nO1xuXHRcdGNvbnN0IHBsYXlBcmVhUmlnaHRFZGdlID0gcGxheUFyZWFMZWZ0RWRnZSArIHRoaXMub3B0aW9ucy5wbGF5QXJlYS53aWR0aDtcblx0XHRjb25zdCBwbGF5QXJlYVRvcEVkZ2UgPSB0aGlzLm9wdGlvbnMucGFkZGluZztcblx0XHRjb25zdCBwbGF5QXJlYUJvdHRvbUVkZ2UgPSBwbGF5QXJlYVRvcEVkZ2UgKyB0aGlzLm9wdGlvbnMucGxheUFyZWEuaGVpZ2h0O1xuXG5cdFx0Y29uc3QgbGVmdEVkZ2UgPSByZWN0Lng7XG5cdFx0Y29uc3QgcmlnaHRFZGdlID0gcmVjdC54ICsgcmVjdC53aWR0aDtcblx0XHRjb25zdCB0b3BFZGdlID0gcmVjdC55O1xuXHRcdGNvbnN0IGJvdHRvbUVkZ2UgPSByZWN0LnkgKyByZWN0LmhlaWdodDtcblxuXHRcdGNvbnN0IGlzSW5QbGF5QXJlYSA9XG5cdFx0XHRsZWZ0RWRnZSA+PSBwbGF5QXJlYUxlZnRFZGdlICYmXG5cdFx0XHRyaWdodEVkZ2UgPD0gcGxheUFyZWFSaWdodEVkZ2UgJiZcblx0XHRcdGJvdHRvbUVkZ2UgPD0gcGxheUFyZWFCb3R0b21FZGdlICYmXG5cdFx0XHR0b3BFZGdlID49IHBsYXlBcmVhVG9wRWRnZTtcblxuXHRcdGlmICghaXNJblBsYXlBcmVhKSB7XG5cdFx0XHRyZXR1cm4gdHJ1ZTtcblx0XHR9XG5cblx0XHRjb25zdCBmaXhlZFBpZWNlcyA9IHRoaXMucGllY2VzLmZpbHRlcihwaWVjZSA9PiBwaWVjZS5zdGF0ZSA9PT0gJ2ZpeGVkJyk7XG5cdFx0Zm9yIChjb25zdCBwaWVjZSBvZiBmaXhlZFBpZWNlcykge1xuXHRcdFx0Y29uc3QgcG9zaXRpb24gPSB0aGlzLmdldFBpZWNlUG9zaXRpb24ocGllY2UpO1xuXHRcdFx0Y29uc3QgbGF5b3V0ID0gcGllY2UuZ2V0TGF5b3V0KCk7XG5cdFx0XHRmb3IgKGxldCBpID0gMDsgaSA8IGxheW91dC5sZW5ndGg7IGkrKykge1xuXHRcdFx0XHRjb25zdCBibG9ja1RvcEVkZ2UgPSBwb3NpdGlvbi55ICsgKGkgKiBUZXRyaXNzaW1vUGllY2UuYmxvY2tMZW5ndGgpO1xuXHRcdFx0XHRjb25zdCBibG9ja0JvdHRvbUVkZ2UgPSBibG9ja1RvcEVkZ2UgKyBUZXRyaXNzaW1vUGllY2UuYmxvY2tMZW5ndGg7XG5cdFx0XHRcdGZvciAobGV0IGogPSAwOyBqIDwgbGF5b3V0W2ldLmxlbmd0aDsgaisrKSB7XG5cdFx0XHRcdFx0aWYgKGxheW91dFtpXVtqXSA9PT0gMCkge1xuXHRcdFx0XHRcdFx0Y29udGludWU7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0Y29uc3QgYmxvY2tMZWZ0RWRnZSA9IHBvc2l0aW9uLnggKyAoaiAqIFRldHJpc3NpbW9QaWVjZS5ibG9ja0xlbmd0aCk7XG5cdFx0XHRcdFx0Y29uc3QgYmxvY2tSaWdodEVkZ2UgPSBibG9ja0xlZnRFZGdlICsgVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoO1xuXG5cdFx0XHRcdFx0Y29uc3QgaXNDb2xsaXNpb24gPVxuXHRcdFx0XHRcdFx0bGVmdEVkZ2UgPj0gYmxvY2tMZWZ0RWRnZSAmJlxuXHRcdFx0XHRcdFx0cmlnaHRFZGdlIDw9IGJsb2NrUmlnaHRFZGdlICYmXG5cdFx0XHRcdFx0XHRib3R0b21FZGdlIDw9IGJsb2NrQm90dG9tRWRnZSAmJlxuXHRcdFx0XHRcdFx0dG9wRWRnZSA+PSBibG9ja1RvcEVkZ2U7XG5cblx0XHRcdFx0XHRpZiAoaXNDb2xsaXNpb24pIHtcblx0XHRcdFx0XHRcdHJldHVybiB0cnVlO1xuXHRcdFx0XHRcdH1cblx0XHRcdFx0fVxuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG5cdHB1YmxpYyB1cGRhdGUoKSB7XG5cdFx0Y29uc3Qgb3B0aW9ucyA9IHRoaXMub3B0aW9ucztcblx0XHRjb25zdCBjb250ZXh0ID0gdGhpcy5vcHRpb25zLmNvbnRleHQ7XG5cblx0XHRjb250ZXh0LmNsZWFyUmVjdCgwLCAwLCBjYW52YXMud2lkdGgsIGNhbnZhcy5oZWlnaHQpO1xuXG5cdFx0Ly8gZmlsbCBiYWNrZ3JvdW5kXG5cdFx0Y29udGV4dC5maWxsU3R5bGUgPSAncmdiKDIyNCwxNTgsMTA3KSc7XG5cdFx0Y29udGV4dC5maWxsUmVjdCgwLCAwLCBjYW52YXMud2lkdGgsIGNhbnZhcy5oZWlnaHQpO1xuXG5cdFx0Ly8gZHJhdyBzb21lIGdyaWQgbGluZXNcblx0XHRpZiAodHlwZW9mKG9wdGlvbnMuZ3JpZExpbmVzKSA9PT0gJ251bWJlcicpIHtcblx0XHRcdGNvbnRleHQuc3Ryb2tlU3R5bGUgPSAncmdiYSgyNTUsIDI1NSwgMjU1LCAwLjI1KSc7XG5cdFx0XHRjb250ZXh0LmxpbmVXaWR0aCA9IDE7XG5cdFx0XHRjb25zdCBpbmNyZW1lbnQgPSBvcHRpb25zLnBpeGVsU2l6ZSAqIG9wdGlvbnMuZ3JpZExpbmVzO1xuXHRcdFx0Zm9yIChsZXQgaSA9IDA7IGkgPCBvcHRpb25zLmNhbnZhcy53aWR0aDsgaSArPSBpbmNyZW1lbnQpIHtcblx0XHRcdFx0Ly8gZHJhdyB2ZXJ0aWNhbCBsaW5lXG5cdFx0XHRcdGNvbnRleHQuYmVnaW5QYXRoKCk7XG5cdFx0XHRcdGNvbnRleHQubW92ZVRvKGksIDApO1xuXHRcdFx0XHRjb250ZXh0LmxpbmVUbyhpLCBvcHRpb25zLmNhbnZhcy5oZWlnaHQpO1xuXHRcdFx0XHRjb250ZXh0LnN0cm9rZSgpO1xuXHRcdFx0fVxuXHRcdFx0Zm9yIChsZXQgaSA9IDA7IGkgPCBvcHRpb25zLmNhbnZhcy5oZWlnaHQ7IGkgKz0gaW5jcmVtZW50KSB7XG5cdFx0XHRcdC8vIGRyYXcgaG9yaXpvbnRhbCBsaW5lc1xuXHRcdFx0XHRjb250ZXh0LmJlZ2luUGF0aCgpO1xuXHRcdFx0XHRjb250ZXh0Lm1vdmVUbygwLCBpKTtcblx0XHRcdFx0Y29udGV4dC5saW5lVG8ob3B0aW9ucy5jYW52YXMud2lkdGgsIGkpO1xuXHRcdFx0XHRjb250ZXh0LnN0cm9rZSgpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdGNvbnN0IHRvdGFsV2lkdGggPSBNYXRoLmZsb29yKGNhbnZhcy53aWR0aCAvIG9wdGlvbnMucGl4ZWxTaXplKTtcblx0XHRjb25zdCBwYWRkaW5nID0gb3B0aW9ucy5wYWRkaW5nO1xuXG5cdFx0Ly8gZHJhdyBIVUQgaW4gdG9wIHJpZ2h0IGNvcm5lclxuXHRcdHRoaXMuaHVkLnJlbmRlcihjb250ZXh0LCB7XG5cdFx0XHQuLi5vcHRpb25zLFxuXHRcdFx0eDogdG90YWxXaWR0aCAtIHBhZGRpbmcgLSBvcHRpb25zLmh1ZC53aWR0aCxcblx0XHRcdHk6IHBhZGRpbmcsXG5cdFx0fSk7XG5cblx0XHQvLyBkcmF3IHBsYXkgYXJlYVxuXHRcdHRoaXMucGxheUFyZWEucmVuZGVyKGNvbnRleHQsIHtcblx0XHRcdC4uLm9wdGlvbnMsXG5cdFx0XHR4OiBwYWRkaW5nLFxuXHRcdFx0eTogcGFkZGluZyxcblx0XHR9KTtcblxuXHRcdHRoaXMucGllY2VzLmZvckVhY2goKHBpZWNlLCBpKSA9PiB7XG5cdFx0XHRwaWVjZS5yZW5kZXIoY29udGV4dCwge1xuXHRcdFx0XHQuLi5vcHRpb25zLFxuXHRcdFx0XHQuLi50aGlzLmdldFBpZWNlT3JpZ2luKHBpZWNlKSxcblx0XHRcdH0pO1xuXHRcdH0pO1xuXHR9XG5cblx0cHJpdmF0ZSBnZXRQaWVjZU9yaWdpbihwaWVjZTogUmVhZG9ubHk8VGV0cmlzc2ltb1BpZWNlPik6IFRldHJpc3NpbW9Db29yZGluYXRlcyB7XG5cdFx0Y29uc3QgbGVuZ3RoID0gVGV0cmlzc2ltb1BpZWNlLmJsb2NrTGVuZ3RoO1xuXHRcdC8vIFRPRE8gcm90YXRpb24gd2lsbCBjYXVzZSBwcm9ibGVtcywgbmVlZCBhIFwiUGllY2VEZWZpbml0aW9uXCIgb3Igc29tZXRoaW5nIHdpdGggYSBzdGF0aWMgbGF5b3V0XG5cdFx0bGV0IHRyYW5zbGF0ZVggPSBNYXRoLmZsb29yKChwaWVjZS5nZXRCbG9ja1dpZHRoKCkgKiBsZW5ndGgpIC8gMik7XG5cblx0XHQvLyB3ZSBjYW4gb25seSBwZXJmb3JtIG1vdmVtZW50cyBpbiBtdWx0aXBsZXMgb2YgdGhlIGJsb2NrTGVuZ3RoLCBzbyB3ZSBtdXN0IGNsYW1wIGl0XG5cdFx0Ly8gdG8gZW5zdXJlIGl0J3Mgbm90IG9mZnNldFxuXHRcdHRyYW5zbGF0ZVggLT0gKHRyYW5zbGF0ZVggJSBsZW5ndGgpO1xuXHRcdGNvbnN0IHBhZGRpbmcgPSB0aGlzLm9wdGlvbnMucGFkZGluZztcblx0XHRyZXR1cm4ge1xuXHRcdFx0eDogcGFkZGluZyArIE1hdGgucm91bmQodGhpcy5vcHRpb25zLnBsYXlBcmVhLndpZHRoIC8gMikgLSB0cmFuc2xhdGVYLFxuXHRcdFx0eTogcGFkZGluZyxcblx0XHR9O1xuXHR9XG59XG5cbmNsYXNzIFRldHJpc3NpbW9OZXh0Qm94IGltcGxlbWVudHMgVUlFbGVtZW50IHtcblx0cHVibGljIHJlbmRlcihjb250ZXh0OiBDYW52YXNSZW5kZXJpbmdDb250ZXh0MkQsIG9wdGlvbnM6IFVJRWxlbWVudFJlbmRlck9wdGlvbnMpIHtcblxuXHR9XG59XG5cbmNsYXNzIFRldHJpc3NpbW9IVUQgaW1wbGVtZW50cyBVSUVsZW1lbnQge1xuXHRwdWJsaWMgcmVuZGVyKGNvbnRleHQ6IENhbnZhc1JlbmRlcmluZ0NvbnRleHQyRCwgb3B0aW9uczogVUlFbGVtZW50UmVuZGVyT3B0aW9ucykge1xuXHRcdGNvbnN0IHBvc2l0aW9uID0ge1xuXHRcdFx0eDogb3B0aW9ucy54LFxuXHRcdFx0eTogb3B0aW9ucy55LFxuXHRcdFx0d2lkdGg6IG9wdGlvbnMuaHVkLndpZHRoLFxuXHRcdFx0aGVpZ2h0OiBvcHRpb25zLmh1ZC5oZWlnaHQsXG5cdFx0fTtcblx0XHRkcmF3Qm9yZGVyZWRSZWN0KHBvc2l0aW9uLCBjb250ZXh0LCBvcHRpb25zKTtcblx0fVxufVxuXG5jbGFzcyBUZXRyaXNzaW1vUGxheUFyZWEgaW1wbGVtZW50cyBVSUVsZW1lbnQge1xuXHRwdWJsaWMgcmVuZGVyKGNvbnRleHQ6IENhbnZhc1JlbmRlcmluZ0NvbnRleHQyRCwgb3B0aW9uczogVUlFbGVtZW50UmVuZGVyT3B0aW9ucykge1xuXHRcdGNvbnN0IHBvc2l0aW9uID0ge1xuXHRcdFx0eDogb3B0aW9ucy54LFxuXHRcdFx0eTogb3B0aW9ucy55LFxuXHRcdFx0d2lkdGg6IG9wdGlvbnMucGxheUFyZWEud2lkdGgsXG5cdFx0XHRoZWlnaHQ6IG9wdGlvbnMucGxheUFyZWEuaGVpZ2h0LFxuXHRcdH07XG5cdFx0ZHJhd0JvcmRlcmVkUmVjdChwb3NpdGlvbiwgY29udGV4dCwgb3B0aW9ucyk7XG5cdH1cbn1cblxudHlwZSBQaWVjZVN0YXRlID0gJ2luLXBsYXknIHwgJ2ZpeGVkJztcblxuY2xhc3MgVGV0cmlzc2ltb1BpZWNlIGltcGxlbWVudHMgVUlFbGVtZW50IHtcblx0cHVibGljIHJlYWRvbmx5IG5hbWU6IHN0cmluZztcblx0cHJpdmF0ZSByZWFkb25seSBvcmlnaW5hbExheW91dDogUmVhZG9ubHk8UGllY2VMYXlvdXQ+O1xuXHRwcml2YXRlIGxheW91dDogUGllY2VMYXlvdXQ7XG5cdHByaXZhdGUgb2Zmc2V0OiBUZXRyaXNzaW1vQ29vcmRpbmF0ZXMgPSB7IHg6IDAsIHk6IDAgfTtcblx0cHJpdmF0ZSByZWFkb25seSBjb2xvcjogc3RyaW5nO1xuXHRwdWJsaWMgc3RhdGljIHJlYWRvbmx5IGJsb2NrTGVuZ3RoID0gNDtcblx0cHVibGljIHN0YXRlOiBQaWVjZVN0YXRlID0gJ2luLXBsYXknO1xuXG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgSiA9IG5ldyBUZXRyaXNzaW1vUGllY2UoXG5cdFx0J0onLFxuXHRcdFtcblx0XHRcdFswLCAxXSxcblx0XHRcdFswLCAxXSxcblx0XHRcdFsxLCAxXSxcblx0XHRdLFxuXHRcdCdibGFjaycsXG5cdCk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgUzogVGV0cmlzc2ltb1BpZWNlID0gbmV3IFRldHJpc3NpbW9QaWVjZShcblx0XHQnUycsXG5cdFx0W1xuXHRcdFx0WzAsIDEsIDFdLFxuXHRcdFx0WzEsIDEsIDBdLFxuXHRcdF0sXG5cdFx0J2dyZWVuJyxcblx0KTtcblx0cHVibGljIHN0YXRpYyByZWFkb25seSBUOiBUZXRyaXNzaW1vUGllY2UgPSBuZXcgVGV0cmlzc2ltb1BpZWNlKFxuXHRcdCdUJyxcblx0XHRbXG5cdFx0XHRbMSwgMSwgMV0sXG5cdFx0XHRbMCwgMSwgMF0sXG5cdFx0XSxcblx0XHQnYmx1ZScsXG5cdCk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgWjogVGV0cmlzc2ltb1BpZWNlID0gbmV3IFRldHJpc3NpbW9QaWVjZShcblx0XHQnWicsXG5cdFx0W1xuXHRcdFx0WzEsIDEsIDAsXSxcblx0XHRcdFswLCAxLCAxLF0sXG5cdFx0XSxcblx0XHQneWVsbG93Jyxcblx0KTtcblx0cHVibGljIHN0YXRpYyByZWFkb25seSBJOiBUZXRyaXNzaW1vUGllY2UgPSBuZXcgVGV0cmlzc2ltb1BpZWNlKFxuXHRcdCdJJyxcblx0XHRbXG5cdFx0XHRbMSwgMSwgMSwgMV0sXG5cdFx0XSxcblx0XHQnbWFnZW50YScsXG5cdCk7XG5cdHB1YmxpYyBzdGF0aWMgcmVhZG9ubHkgTDogVGV0cmlzc2ltb1BpZWNlID0gbmV3IFRldHJpc3NpbW9QaWVjZShcblx0XHQnTCcsXG5cdFx0W1xuXHRcdFx0WzFdLFxuXHRcdFx0WzFdLFxuXHRcdFx0WzEsIDFdLFxuXHRcdF0sXG5cdFx0J2N5YW4nLFxuXHQpO1xuXHRwdWJsaWMgc3RhdGljIHJlYWRvbmx5IE86IFRldHJpc3NpbW9QaWVjZSA9IG5ldyBUZXRyaXNzaW1vUGllY2UoXG5cdFx0J08nLFxuXHRcdFtcblx0XHRcdFsxLCAxXSxcblx0XHRcdFsxLCAxXSxcblx0XHRdLFxuXHRcdCdyZWQnLFxuXHQpO1xuXG5cdHB1YmxpYyBjb25zdHJ1Y3RvcihuYW1lOiBzdHJpbmcsIGxheW91dDogUGllY2VMYXlvdXQsIGNvbG9yOiBzdHJpbmcpIHtcblx0XHR0aGlzLm5hbWUgPSBuYW1lO1xuXHRcdHRoaXMub3JpZ2luYWxMYXlvdXQgPSBsYXlvdXQubWFwKHJvdyA9PiByb3cuY29uY2F0KFtdKSk7XG5cdFx0dGhpcy5sYXlvdXQgPSBsYXlvdXQuY29uY2F0KFtdKTtcblx0XHR0aGlzLmNvbG9yID0gY29sb3I7XG5cdH1cblxuXHRwdWJsaWMgZ2V0T2Zmc2V0KCk6IFJlYWRvbmx5PFRldHJpc3NpbW9Db29yZGluYXRlcz4ge1xuXHRcdHJldHVybiB0aGlzLm9mZnNldDtcblx0fVxuXG5cdHB1YmxpYyBnZXRMYXlvdXQoKTogUmVhZG9ubHk8UGllY2VMYXlvdXQ+IHtcblx0XHRyZXR1cm4gdGhpcy5sYXlvdXQ7XG5cdH1cblxuXHRwdWJsaWMgbW92ZSh0cmFuc2xhdGlvbjogUGllY2VUcmFuc2xhdGlvbikge1xuXHRcdGlmICh0aGlzLnN0YXRlICE9PSAnaW4tcGxheScpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cblx0XHR0aGlzLm9mZnNldC54ICs9ICh0cmFuc2xhdGlvbi54RGVsdGEgKiBUZXRyaXNzaW1vUGllY2UuYmxvY2tMZW5ndGgpO1xuXHRcdHRoaXMub2Zmc2V0LnkgKz0gKHRyYW5zbGF0aW9uLnlEZWx0YSAqIFRldHJpc3NpbW9QaWVjZS5ibG9ja0xlbmd0aCk7XG5cdH1cblxuXHRwdWJsaWMgcm90YXRlTGVmdCgpIHtcblx0XHRpZiAodGhpcy5zdGF0ZSAhPT0gJ2luLXBsYXknKSB7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXG5cdFx0Ly8gY2xvY2t3aXNlIHR1cm5cblx0XHRjb25zdCBuZXdMYXlvdXQ6IFBpZWNlTGF5b3V0ID0gW107XG5cdFx0Zm9yIChsZXQgaSA9IDA7IGkgPCB0aGlzLmxheW91dC5sZW5ndGg7IGkrKykge1xuXHRcdFx0Y29uc3QgY29sID0gdGhpcy5sYXlvdXQubGVuZ3RoIC0gMSAtIGk7XG5cdFx0XHRmb3IgKGxldCBqID0gMDsgaiA8IHRoaXMubGF5b3V0W2ldLmxlbmd0aDsgaisrKSB7XG5cdFx0XHRcdGNvbnN0IHJvdyA9IGo7XG5cdFx0XHRcdGlmICghbmV3TGF5b3V0W3Jvd10pIHtcblx0XHRcdFx0XHRuZXdMYXlvdXRbcm93XSA9IFtdO1xuXHRcdFx0XHR9XG5cdFx0XHRcdG5ld0xheW91dFtyb3ddW2NvbF0gPSB0aGlzLmxheW91dFtpXVtqXTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHR0aGlzLmxheW91dCA9IG5ld0xheW91dDtcblx0fVxuXG5cdHB1YmxpYyByb3RhdGVSaWdodCgpIHtcblx0XHRpZiAodGhpcy5zdGF0ZSAhPT0gJ2luLXBsYXknKSB7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXG5cdFx0Y29uc3QgbmV3TGF5b3V0OiBQaWVjZUxheW91dCA9IFtdO1xuXHRcdGZvciAobGV0IGkgPSAwOyBpIDwgdGhpcy5sYXlvdXQubGVuZ3RoOyBpKyspIHtcblx0XHRcdGNvbnN0IGNvbCA9IGk7XG5cdFx0XHRmb3IgKGxldCBqID0gMDsgaiA8IHRoaXMubGF5b3V0W2ldLmxlbmd0aDsgaisrKSB7XG5cdFx0XHRcdGNvbnN0IHJvdyA9IHRoaXMubGF5b3V0W2ldLmxlbmd0aCAtIDEgLSBqO1xuXHRcdFx0XHRpZiAoIW5ld0xheW91dFtyb3ddKSB7XG5cdFx0XHRcdFx0bmV3TGF5b3V0W3Jvd10gPSBbXTtcblx0XHRcdFx0fVxuXHRcdFx0XHRuZXdMYXlvdXRbcm93XVtjb2xdID0gdGhpcy5sYXlvdXRbaV1bal07XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0dGhpcy5sYXlvdXQgPSBuZXdMYXlvdXQ7XG5cdH1cblxuXHRwdWJsaWMgc2V0Rml4ZWQoKSB7XG5cdFx0dGhpcy5zdGF0ZSA9ICdmaXhlZCc7XG5cdH1cblxuXHRwdWJsaWMgZ2V0QmxvY2tXaWR0aCgpOiBudW1iZXIge1xuXHRcdHJldHVybiB0aGlzLmxheW91dFxuXHRcdFx0Lm1hcChhcnIgPT4gYXJyLmZpbHRlcih2YWx1ZSA9PiB2YWx1ZSA9PT0gMSkubGVuZ3RoKVxuXHRcdFx0LnJlZHVjZSgobWF4LCBsZW4pID0+IE1hdGgubWF4KG1heCwgbGVuKSwgMCk7XG5cdH1cblxuXHRwdWJsaWMgZ2V0QmxvY2tIZWlnaHQoKTogbnVtYmVyIHtcblx0XHRjb25zdCBjb2xzOiBQaWVjZUNlbGxbXVtdID0gW107XG5cdFx0Zm9yIChsZXQgaSA9IDA7IGkgPCB0aGlzLmxheW91dC5sZW5ndGg7IGkrKykge1xuXHRcdFx0Zm9yIChsZXQgaiA9IDA7IGogPCB0aGlzLmxheW91dFtpXS5sZW5ndGg7IGorKykge1xuXHRcdFx0XHRpZiAoIWNvbHNbal0pIHtcblx0XHRcdFx0XHRjb2xzW2pdID0gW107XG5cdFx0XHRcdH1cblx0XHRcdFx0Y29sc1tqXS5wdXNoKHRoaXMubGF5b3V0W2ldW2pdKTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRyZXR1cm4gY29sc1xuXHRcdFx0Lm1hcChhcnIgPT4gYXJyLmZpbHRlcih2YWx1ZSA9PiB2YWx1ZSA9PT0gMSkubGVuZ3RoKVxuXHRcdFx0LnJlZHVjZSgobWF4LCBsZW4pID0+IE1hdGgubWF4KG1heCwgbGVuKSwgMCk7XG5cdH1cblxuXHRwdWJsaWMgY2xvbmUoKTogVGV0cmlzc2ltb1BpZWNlIHtcblx0XHRyZXR1cm4gbmV3IFRldHJpc3NpbW9QaWVjZSh0aGlzLm5hbWUsIHRoaXMubGF5b3V0LCB0aGlzLmNvbG9yKTtcblx0fVxuXG5cdHB1YmxpYyByZW5kZXIoY29udGV4dDogQ2FudmFzUmVuZGVyaW5nQ29udGV4dDJELCBvcHRpb25zOiBVSUVsZW1lbnRSZW5kZXJPcHRpb25zKSB7XG5cdFx0dGhpcy5sYXlvdXQuZm9yRWFjaCgoY29sdW1ucywgcm93TnVtKSA9PiB7XG5cdFx0XHRjb2x1bW5zLmZvckVhY2goKHZhbHVlLCBjb2xOdW0pID0+IHtcblx0XHRcdFx0c3dpdGNoICh2YWx1ZSkge1xuXHRcdFx0XHRcdGNhc2UgMDpcblx0XHRcdFx0XHRcdGJyZWFrO1xuXHRcdFx0XHRcdGNhc2UgMTpcblx0XHRcdFx0XHRcdGNvbnRleHQuZmlsbFN0eWxlID0gdGhpcy5jb2xvcjtcblx0XHRcdFx0XHRcdGNvbnN0IGxlbmd0aCA9IFRldHJpc3NpbW9QaWVjZS5ibG9ja0xlbmd0aDtcblx0XHRcdFx0XHRcdGNvbnN0IG9mZnNldFggPSB0aGlzLm9mZnNldC54O1xuXHRcdFx0XHRcdFx0Y29uc3Qgb2Zmc2V0WSA9IHRoaXMub2Zmc2V0Lnk7XG5cblx0XHRcdFx0XHRcdGNvbnN0IHggPSBvcHRpb25zLnggKyBvZmZzZXRYICsgKGNvbE51bSAqIGxlbmd0aCk7XG5cdFx0XHRcdFx0XHRjb25zdCB5ID0gb3B0aW9ucy55ICsgb2Zmc2V0WSArIChyb3dOdW0gKiBsZW5ndGgpO1xuXHRcdFx0XHRcdFx0Y29uc3QgYWN0dWFsTGVuZ3RoID0gZ2V0U2l6ZShsZW5ndGgsIG9wdGlvbnMpO1xuXHRcdFx0XHRcdFx0Y29udGV4dC5maWxsUmVjdChnZXRTaXplKHgsIG9wdGlvbnMpLCBnZXRTaXplKHksIG9wdGlvbnMpLCBhY3R1YWxMZW5ndGgsIGFjdHVhbExlbmd0aCk7XG5cdFx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0XHRkZWZhdWx0OlxuXHRcdFx0XHRcdFx0bm9wZSh2YWx1ZSk7XG5cdFx0XHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcGllY2UgY2VsbCB2YWx1ZSBcIiR7dmFsdWV9XCJgKTtcblx0XHRcdFx0fVxuXHRcdFx0fSk7XG5cdFx0fSk7XG5cdH1cbn1cblxuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbmNvbnN0IGh1ZCA9IG5ldyBUZXRyaXNzaW1vSFVEKCk7XG5jb25zdCBwbGF5QXJlYSA9IG5ldyBUZXRyaXNzaW1vUGxheUFyZWEoKTtcblxuY29uc3QgY2FudmFzID0gZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoJ2NhbnZhcycpWzBdO1xuaWYgKCFjYW52YXMpIHtcblx0dGhyb3cgbmV3IEVycm9yKCdubyBjYW52YXMnKTtcbn1cblxuY29uc3QgdWkgPSBuZXcgVGV0cmlzc2ltb1VJKGNhbnZhcywgcGxheUFyZWEsIGh1ZCk7XG5jb25zdCBnYW1lID0gbmV3IFRldHJpc3NpbW9HYW1lKHVpKTtcblxuIl19 \ No newline at end of file diff --git a/tetrissimo.ts b/tetrissimo.ts new file mode 100644 index 0000000..6abf483 --- /dev/null +++ b/tetrissimo.ts @@ -0,0 +1,764 @@ +type GameState = 'running' | 'paused' | 'not-started' | 'finished'; + +const nope = (x: never): void => {}; + +type PieceCell = 0 | 1; +type PieceLayout = PieceCell[][]; + +interface UIElement { + render(context: CanvasRenderingContext2D, options: UIElementRenderOptions): void; +} + +interface Dimensions { + width: number; + height: number; +} + +interface GlobalUIOptions { + pixelSize: number; + padding: number; + canvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + gridLines: false | number; + borderWidth: number; + hud: Dimensions; + playArea: Dimensions; +} + +interface UIElementRenderOptions extends GlobalUIOptions { + x: number; + y: number; +} + +interface PieceTranslation { + xDelta: number; + yDelta: number; +} + +interface TetrissimoCoordinates { + x: number; + y: number; +} + +interface RectangleCoordinates extends TetrissimoCoordinates, Dimensions {} + +type PieceActionType = + 'moveLeft' | + 'moveRight' | + 'moveDown' | + 'rotateLeft' | + 'rotateRight'; + +interface PieceAction { + type: PieceActionType; +} + +const getSize = (value: number, options: Pick): number => value * options.pixelSize; + +const drawBorderedRect = ( + position: Dimensions & TetrissimoCoordinates, + context: CanvasRenderingContext2D, + options: UIElementRenderOptions, +): void => { + const borderWidth = options.borderWidth; + if (!borderWidth) { + return; + } + + const actualLineWidth = getSize(borderWidth, options); + context.lineWidth = actualLineWidth; + context.strokeStyle = 'white'; + context.strokeRect( + getSize(position.x, options) - Math.round(actualLineWidth / 2), + getSize(position.y, options) - Math.round(actualLineWidth / 2), + getSize(position.width, options) + actualLineWidth, + getSize(position.height, options) + actualLineWidth, + ); +}; + +class TetrissimoLevel { + public readonly order: number; + public readonly movementsPerSecond: number; + + public static readonly level1 = new TetrissimoLevel(1, 0.5); + public static readonly level2 = new TetrissimoLevel(2, 0.75); + public static readonly level3 = new TetrissimoLevel(2, 1); + public static readonly level4 = new TetrissimoLevel(2, 1.5); + public static readonly level5 = new TetrissimoLevel(2, 2); + public static readonly level6 = new TetrissimoLevel(2, 3); + public static readonly level7 = new TetrissimoLevel(2, 4); + public static readonly level8 = new TetrissimoLevel(2, 6); + public static readonly level9 = new TetrissimoLevel(2, 8); + public static readonly level10 = new TetrissimoLevel(2, 10); + + public constructor(order: number, speed: number) { + this.order = order; + this.movementsPerSecond = speed; + } +} + +class TetrissimoGame { + private startedAt: Date | null = null; + private pausedAt: Date | null = null; + private finishedAt: Date | null = null; + private state: GameState = 'not-started'; + private fps = 60; + private readonly ui: TetrissimoUI; + private lastFrameTimes: number[] = []; + private readonly pieces: TetrissimoPiece[] = []; + public currentLevel: TetrissimoLevel = TetrissimoLevel.level1; + private msUntilNextTick = 0; + private lastTick = new Date(); + private lastUpdate = new Date(); + + public constructor(ui: TetrissimoUI) { + this.ui = ui; + } + + public getState(): GameState { + return this.state; + } + + public setTargetFps(fps: number) { + this.fps = fps; + } + + public setLevel(level: TetrissimoLevel) { + this.currentLevel = level; + } + + public getCurrentFps(): number { + switch (this.state) { + case 'running': + if (this.lastFrameTimes.length <= 1) { + return 0; + } + + const firstFrame = this.lastFrameTimes[0]; + const lastFrame = this.lastFrameTimes[this.lastFrameTimes.length - 1]; + const elapsedMs = lastFrame - firstFrame; + const avgMsPerFrame = (elapsedMs / this.lastFrameTimes.length); + return 1000 / avgMsPerFrame; + case 'paused': + case 'not-started': + case 'finished': + return 0; + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + + public performPieceAction(action: PieceAction) { + if (this.state !== 'running') { + return; + } + + const piece = this.pieces.filter(piece => piece.state === 'in-play')[0]; + if (!piece) { + // nothing to do, no pieces in play + return; + } + + const movementIncrement = 1; + + switch (action.type) { + case 'moveLeft': + this.moveIfValid(piece, -movementIncrement, 0); + break; + case 'moveRight': + this.moveIfValid(piece, movementIncrement, 0); + break; + case 'moveDown': + if (!this.moveIfValid(piece, 0, movementIncrement)) { + // make fixed to bottom of play area + piece.setFixed(); + } + break; + case 'rotateLeft': + // TODO ensure it's a valid move + piece.rotateLeft(); + break; + case 'rotateRight': + piece.rotateRight(); + break; + default: + nope(action.type); + throw new Error(`invalid action "${action.type}"`); + } + } + + public start() { + this.pausedAt = null; + + switch (this.state) { + case 'running': + // no-op + break; + case 'paused': + case 'not-started': + this.lastTick = new Date(); + this.lastUpdate = new Date(); + this.startedAt = new Date(); + this.state = 'running'; + this.update(); + break; + case 'finished': + throw new Error('Cannot start a finished game'); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + + public pause() { + switch (this.state) { + case 'running': + case 'paused': + this.pausedAt = new Date(); + this.state = 'paused'; + this.lastFrameTimes = []; + break; + case 'not-started': + case 'finished': + throw new Error(`Cannot pause game while it's in state "${this.state}"`); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + } + + public update(): void { + const tickStart = Date.now(); + this.lastFrameTimes.push(tickStart); + if (this.lastFrameTimes.length > 20) { + this.lastFrameTimes.shift(); + } + + switch (this.state) { + case 'running': + this.performUpdate(); + break; + case 'paused': + case 'not-started': + case 'finished': + throw new Error(`Cannot advance game while in state "${this.state}"`); + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + + const elapsed = Date.now() - tickStart; + const nextTickTimeout = Math.max(0, (1000 / this.fps) - elapsed); + setTimeout(() => { + switch (this.state) { + case 'running': + this.update(); + break; + case 'paused': + case 'not-started': + case 'finished': + break; + default: + nope(this.state); + throw new Error(`Invalid game state "${this.state}"`); + } + }, nextTickTimeout); + } + + private performUpdate(): void { + const sinceLastUpdate = Date.now() - this.lastUpdate.getTime(); + this.msUntilNextTick -= sinceLastUpdate; + this.lastUpdate = new Date(); + + if (this.msUntilNextTick <= 0) { + this.tick(); + } + + // detect collision + // is every column filled? remove column + // is every row filled? end game + + ui.update(); + } + + private moveIfValid(piece: TetrissimoPiece, xDelta: number, yDelta: number): boolean { + const translation: PieceTranslation = { + xDelta, + yDelta, + }; + if (this.ui.canMovePiece(piece, translation)) { + piece.move(translation); + return true; + } + + return false; + }; + + private tick(): void { + const inPlayPieces = this.pieces.filter(piece => piece.state === 'in-play'); + if (!inPlayPieces.length) { + const newPiece = TetrissimoPiece.T.clone(); + newPiece.state = 'in-play'; + this.pieces.push(newPiece); + this.ui.addPiece(newPiece); + } else { + inPlayPieces.forEach((piece) => { + if (!this.moveIfValid(piece, 0, 1)) { + piece.setFixed(); + } + }); + } + + this.msUntilNextTick = Math.round(1000 / this.currentLevel.movementsPerSecond); + this.lastTick = new Date(); + } +} + +class TetrissimoUI { + public readonly playArea: TetrissimoPlayArea; + private readonly pieces: Array> = []; + public readonly nextBox: TetrissimoNextBox; + public readonly hud: TetrissimoHUD; + public readonly options: GlobalUIOptions; + + public constructor( + canvas: HTMLCanvasElement, + playArea: TetrissimoPlayArea, + // nextBox: TetrissimoNextBox, + hud: TetrissimoHUD, + ) { + this.playArea = playArea; + // this.nextBox = nextBox; + this.hud = hud; + + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('failed to get 2d context'); + } + this.options = { + borderWidth: 1, + canvas: canvas, + context, + gridLines: 4, + hud: { + width: 32, + height: 12, + }, + playArea: { + width: 68, + height: 128, + }, + padding: 2, + pixelSize: 4, + }; + } + + public setOptions(options: Omit) { + Object.keys(options).forEach((key) => { + this.options[key] = options[key]; + }); + } + + public addPiece(piece: TetrissimoPiece) { + this.pieces.push(piece); + } + + private getPiecePosition(piece: Readonly): TetrissimoCoordinates { + const origin = this.getPieceOrigin(piece); + const offset = piece.getOffset(); + + return { + x: origin.x + offset.x, + y: origin.y + offset.y, + }; + } + + public canMovePiece(piece: Readonly, delta: PieceTranslation): boolean { + // check each block in layout's edge to see if it's in a valid area?s + + const layout = piece.getLayout(); + const position = this.getPiecePosition(piece); + + for (let i = 0; i < layout.length; i++) { + const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength) + (delta.yDelta * TetrissimoPiece.blockLength); + for (let j = 0; j < layout[i].length; j++) { + if (layout[i][j] === 0) { + continue; + } + + const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength) + (delta.xDelta * TetrissimoPiece.blockLength); + + const isCollision = this.hasCollision({ + x: blockLeftEdge, + y: blockTopEdge, + width: TetrissimoPiece.blockLength, + height: TetrissimoPiece.blockLength, + }); + + if (isCollision) { + return false; + } + } + } + + return true; + } + + private hasCollision(rect: RectangleCoordinates): boolean { + const playAreaLeftEdge = this.options.padding; + const playAreaRightEdge = playAreaLeftEdge + this.options.playArea.width; + const playAreaTopEdge = this.options.padding; + const playAreaBottomEdge = playAreaTopEdge + this.options.playArea.height; + + const leftEdge = rect.x; + const rightEdge = rect.x + rect.width; + const topEdge = rect.y; + const bottomEdge = rect.y + rect.height; + + const isInPlayArea = + leftEdge >= playAreaLeftEdge && + rightEdge <= playAreaRightEdge && + bottomEdge <= playAreaBottomEdge && + topEdge >= playAreaTopEdge; + + if (!isInPlayArea) { + return true; + } + + const fixedPieces = this.pieces.filter(piece => piece.state === 'fixed'); + for (const piece of fixedPieces) { + const position = this.getPiecePosition(piece); + const layout = piece.getLayout(); + for (let i = 0; i < layout.length; i++) { + const blockTopEdge = position.y + (i * TetrissimoPiece.blockLength); + const blockBottomEdge = blockTopEdge + TetrissimoPiece.blockLength; + for (let j = 0; j < layout[i].length; j++) { + if (layout[i][j] === 0) { + continue; + } + + const blockLeftEdge = position.x + (j * TetrissimoPiece.blockLength); + const blockRightEdge = blockLeftEdge + TetrissimoPiece.blockLength; + + const isCollision = + leftEdge >= blockLeftEdge && + rightEdge <= blockRightEdge && + bottomEdge <= blockBottomEdge && + topEdge >= blockTopEdge; + + if (isCollision) { + return true; + } + } + } + } + + return false; + } + + public update() { + const options = this.options; + const context = this.options.context; + + context.clearRect(0, 0, canvas.width, canvas.height); + + // fill background + context.fillStyle = 'rgb(224,158,107)'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // draw some grid lines + if (typeof(options.gridLines) === 'number') { + context.strokeStyle = 'rgba(255, 255, 255, 0.25)'; + context.lineWidth = 1; + const increment = options.pixelSize * options.gridLines; + for (let i = 0; i < options.canvas.width; i += increment) { + // draw vertical line + context.beginPath(); + context.moveTo(i, 0); + context.lineTo(i, options.canvas.height); + context.stroke(); + } + for (let i = 0; i < options.canvas.height; i += increment) { + // draw horizontal lines + context.beginPath(); + context.moveTo(0, i); + context.lineTo(options.canvas.width, i); + context.stroke(); + } + } + + const totalWidth = Math.floor(canvas.width / options.pixelSize); + const padding = options.padding; + + // draw HUD in top right corner + this.hud.render(context, { + ...options, + x: totalWidth - padding - options.hud.width, + y: padding, + }); + + // draw play area + this.playArea.render(context, { + ...options, + x: padding, + y: padding, + }); + + this.pieces.forEach((piece, i) => { + piece.render(context, { + ...options, + ...this.getPieceOrigin(piece), + }); + }); + } + + private getPieceOrigin(piece: Readonly): TetrissimoCoordinates { + const length = TetrissimoPiece.blockLength; + // TODO rotation will cause problems, need a "PieceDefinition" or something with a static layout + let translateX = Math.floor((piece.getBlockWidth() * length) / 2); + + // we can only perform movements in multiples of the blockLength, so we must clamp it + // to ensure it's not offset + translateX -= (translateX % length); + const padding = this.options.padding; + return { + x: padding + Math.round(this.options.playArea.width / 2) - translateX, + y: padding, + }; + } +} + +class TetrissimoNextBox implements UIElement { + public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) { + + } +} + +class TetrissimoHUD implements UIElement { + public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) { + const position = { + x: options.x, + y: options.y, + width: options.hud.width, + height: options.hud.height, + }; + drawBorderedRect(position, context, options); + } +} + +class TetrissimoPlayArea implements UIElement { + public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) { + const position = { + x: options.x, + y: options.y, + width: options.playArea.width, + height: options.playArea.height, + }; + drawBorderedRect(position, context, options); + } +} + +type PieceState = 'in-play' | 'fixed'; + +class TetrissimoPiece implements UIElement { + public readonly name: string; + private readonly originalLayout: Readonly; + private layout: PieceLayout; + private offset: TetrissimoCoordinates = { x: 0, y: 0 }; + private readonly color: string; + public static readonly blockLength = 4; + public state: PieceState = 'in-play'; + + public static readonly J = new TetrissimoPiece( + 'J', + [ + [0, 1], + [0, 1], + [1, 1], + ], + 'black', + ); + public static readonly S: TetrissimoPiece = new TetrissimoPiece( + 'S', + [ + [0, 1, 1], + [1, 1, 0], + ], + 'green', + ); + public static readonly T: TetrissimoPiece = new TetrissimoPiece( + 'T', + [ + [1, 1, 1], + [0, 1, 0], + ], + 'blue', + ); + public static readonly Z: TetrissimoPiece = new TetrissimoPiece( + 'Z', + [ + [1, 1, 0,], + [0, 1, 1,], + ], + 'yellow', + ); + public static readonly I: TetrissimoPiece = new TetrissimoPiece( + 'I', + [ + [1, 1, 1, 1], + ], + 'magenta', + ); + public static readonly L: TetrissimoPiece = new TetrissimoPiece( + 'L', + [ + [1], + [1], + [1, 1], + ], + 'cyan', + ); + public static readonly O: TetrissimoPiece = new TetrissimoPiece( + 'O', + [ + [1, 1], + [1, 1], + ], + 'red', + ); + + public constructor(name: string, layout: PieceLayout, color: string) { + this.name = name; + this.originalLayout = layout.map(row => row.concat([])); + this.layout = layout.concat([]); + this.color = color; + } + + public getOffset(): Readonly { + return this.offset; + } + + public getLayout(): Readonly { + return this.layout; + } + + public move(translation: PieceTranslation) { + if (this.state !== 'in-play') { + return; + } + + this.offset.x += (translation.xDelta * TetrissimoPiece.blockLength); + this.offset.y += (translation.yDelta * TetrissimoPiece.blockLength); + } + + public rotateLeft() { + if (this.state !== 'in-play') { + return; + } + + // clockwise turn + const newLayout: PieceLayout = []; + for (let i = 0; i < this.layout.length; i++) { + const col = this.layout.length - 1 - i; + for (let j = 0; j < this.layout[i].length; j++) { + const row = j; + if (!newLayout[row]) { + newLayout[row] = []; + } + newLayout[row][col] = this.layout[i][j]; + } + } + + this.layout = newLayout; + } + + public rotateRight() { + if (this.state !== 'in-play') { + return; + } + + const newLayout: PieceLayout = []; + for (let i = 0; i < this.layout.length; i++) { + const col = i; + for (let j = 0; j < this.layout[i].length; j++) { + const row = this.layout[i].length - 1 - j; + if (!newLayout[row]) { + newLayout[row] = []; + } + newLayout[row][col] = this.layout[i][j]; + } + } + + this.layout = newLayout; + } + + public setFixed() { + this.state = 'fixed'; + } + + public getBlockWidth(): number { + return this.layout + .map(arr => arr.filter(value => value === 1).length) + .reduce((max, len) => Math.max(max, len), 0); + } + + public getBlockHeight(): number { + const cols: PieceCell[][] = []; + for (let i = 0; i < this.layout.length; i++) { + for (let j = 0; j < this.layout[i].length; j++) { + if (!cols[j]) { + cols[j] = []; + } + cols[j].push(this.layout[i][j]); + } + } + + return cols + .map(arr => arr.filter(value => value === 1).length) + .reduce((max, len) => Math.max(max, len), 0); + } + + public clone(): TetrissimoPiece { + return new TetrissimoPiece(this.name, this.layout, this.color); + } + + public render(context: CanvasRenderingContext2D, options: UIElementRenderOptions) { + this.layout.forEach((columns, rowNum) => { + columns.forEach((value, colNum) => { + switch (value) { + case 0: + break; + case 1: + context.fillStyle = this.color; + const length = TetrissimoPiece.blockLength; + const offsetX = this.offset.x; + const offsetY = this.offset.y; + + const x = options.x + offsetX + (colNum * length); + const y = options.y + offsetY + (rowNum * length); + const actualLength = getSize(length, options); + context.fillRect(getSize(x, options), getSize(y, options), actualLength, actualLength); + break; + default: + nope(value); + throw new Error(`Invalid piece cell value "${value}"`); + } + }); + }); + } +} + +// --------------------------------------------- + +const hud = new TetrissimoHUD(); +const playArea = new TetrissimoPlayArea(); + +const canvas = document.getElementsByTagName('canvas')[0]; +if (!canvas) { + throw new Error('no canvas'); +} + +const ui = new TetrissimoUI(canvas, playArea, hud); +const game = new TetrissimoGame(ui); + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c1c34ef --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "es6", + "module": "amd", + "lib": [ + "es6", + "dom" + ], + "moduleResolution": "node", + "noImplicitAny": false, + "allowJs": false, + "inlineSourceMap": true, + "inlineSources": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": true, + "noImplicitReturns": false, + "preserveConstEnums": true, + "strictNullChecks": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "forceConsistentCasingInFileNames": true + } +}