diff --git a/common/src/algorithms/movement.spec.ts b/common/src/algorithms/movement.spec.ts new file mode 100644 index 0000000..8c01e1f --- /dev/null +++ b/common/src/algorithms/movement.spec.ts @@ -0,0 +1,157 @@ +import { Board } from "../types/GameBoard"; +import { expect } from "chai"; +import "mocha"; + +import { countShips, shipLocation } from "./movement"; + +function stringifyBoard(b: Board) { + return ( + b.singularity.join(`\t`) + + ` > ` + + [...b.path].map(x => x != null ? x : `-`).join(`\t`) + + ` > ` + + b.warpgate.join(`\t`) + ); +}; + + +describe("The shipLocation function", () => { + + const tests = [ + { + name: `Should correctly identify the singularity location`, + args: ["a"], + input: { + singularity: ["a"], + path: [], + warpgate: [], + }, + expect: "singularity", + }, + { + name: `Should correctly identify the path location`, + args: ["a"], + input: { + singularity: [], + path: ["a"], + warpgate: [], + }, + expect: "path", + }, + { + name: `Should correctly identify the warpgate location`, + args: ["a"], + input: { + singularity: [], + path: [], + warpgate: ["a"], + }, + expect: "warpgate", + }, + { + name: `Should correctly identify the ship doesn't exist`, + args: ["a"], + input: { + singularity: [], + path: [], + warpgate: [], + }, + expect: null, + }, + ]; + + for (const test of tests) { + it(test.name, () => { + let b = JSON.parse(JSON.stringify(test.input)); + + let location = shipLocation(b, test.args[0]); + expect(location).to.equal(test.expect); + }); + }; +}); + + +describe("The countShips function", () => { + + const tests = [ + { + name: `shouldn't could ships in the singularity or warpgate`, + args: [3], + input: { + singularity: ["a"], + path: [null, null, null, "b", null, null, null], + warpgate: ["c"], + }, + shouldError: false, + expect: { left: 0, right: 0 }, + }, + { + name: `should count the ships in the path correctly when location is in the middle`, + args: [3], + input: { + singularity: ["a"], + path: ["d", null, "c", "b", null, null, "e"], + warpgate: [], + }, + shouldError: false, + expect: { left: 2, right: 1 }, + }, + { + name: `should count the ships in the path correctly when location is on the low edge`, + args: [0], + input: { + singularity: ["a"], + path: ["d", null, "c", "b", null, null, "e"], + warpgate: [], + }, + shouldError: false, + expect: { left: 0, right: 3 }, + }, + { + name: `should count the ships in the path correctly when location is on the high edge`, + args: [6], + input: { + singularity: ["a"], + path: ["d", null, "c", "b", null, null, "e"], + warpgate: [], + }, + shouldError: false, + expect: { left: 3, right: 0 }, + }, + { + name: `should error when location is negative`, + args: [-1], + input: { + singularity: [], + path: ["d", null, "c", "b", null, null, "e"], + warpgate: [], + }, + expect: new Error("location can't be negative"), + }, + { + name: `should error when location is too high`, + args: [100], + input: { + singularity: [], + path: ["d", null, "c", "b", null, null, "e"], + warpgate: [], + }, + expect: new Error("location can't be larger than the path"), + }, + ]; + + for (const test of tests) { + it(test.name, () => { + let b = JSON.parse(JSON.stringify(test.input)); + if (test.expect instanceof Error) { + expect(countShips(b, test.args[0])).to.throw(test.expect); + } else { + + let count = countShips(b, test.args[0]); + expect(count).to.have.all.keys(Object.keys(test.expect)); + expect(count.left).to.equal(test.expect.left); + expect(count.right).to.equal(test.expect.right); + }; + }); + }; +}); \ No newline at end of file diff --git a/common/src/algorithms/movement.ts b/common/src/algorithms/movement.ts new file mode 100644 index 0000000..68314b5 --- /dev/null +++ b/common/src/algorithms/movement.ts @@ -0,0 +1,201 @@ +import { FuelCard } from "../index"; +import { GamePiece, Board } from "../index"; + + +/** @internal */ +export function shipLocation(board: Board, ship: GamePiece) { + if (board.path.includes(ship)) { + return "path"; + }; + if (board.singularity.includes(ship)) { + return "singularity"; + }; + if (board.warpgate.includes(ship)) { + return "warpgate"; + }; + return null; +}; + + +/** @internal */ +export function countShips(board: Board, location: number) { + + if (location < 0) { + throw new Error("location can't be negative"); + }; + if (location > board.path.length) { + throw new Error("location can't be larger than the path"); + }; + + let left = 0; + if (location > 0) { + left = board.path.slice(0, location - 1).filter(x => x != null).length; + }; + + return { + left, + right: board.path.slice(location + 1).filter(x => x != null).length, + }; +}; + + +/** @internal */ +export function determineDirection(board: Board, location: number): number { + + /* + Edge-Case check for when the board only has a single ship on it because any + ships that hit the warp gate get removed from the game, and ships in the + singularity don't create gravity wells, not affecting other ships. + */ + let shipCount = 0; + for (const location of board.path) { + if (location != null) { + shipCount++; + if (shipCount >= 2) { + break; + }; + }; + }; + if (shipCount <= 1) { + return 1; + }; + + let delta = 1; + while (true) { + if (location - delta < 0) { + return 1; + } else if (location + delta >= board.path.length) { + return -1; + } else { + let left = board.path[location - delta]; + let right = board.path[location + delta]; + + if (left != null && right != null) { + let sum = countShips(board, location) + return Math.sign(sum.right - sum.left); + } else if (left != null) { + return -1; + } else if (right != null) { + return 1; + }; + + delta++; + }; + }; +}; + + +/** @internal */ +export function tractorBeam(board: Board, ship: GamePiece, card: FuelCard) { + let delta = 0; + + while (true) { + delta++; + + let behind = origin - delta; + let ahead = origin + delta; + + let negativeNewPosition = behind; + let positiveNewPosition = ahead; + let behindShip = board[behind]; + let aheadShip = board[ahead]; + + /* + Check for a ship behind the origin, if there is a ship there then pull + it towards the origin (forwards). If it were to collide with another + ship, then drift until it no longer collides. + */ + if ( + behind >= 0 + && behindShip != null + ) { + let localMagnitude = magnitude; + while (board[behind + localMagnitude] != null) { + localMagnitude++; + }; + negativeNewPosition = behind + localMagnitude; + board[behind] = null; + }; + + /* + Check for a ship ahead of the origin, if there is a ship there then + pull it towards the origin (backwards). If it were to collide with + another ship, then drift until it no longer collides. + */ + if ( + ahead < board.length + && aheadShip + ) { + let localMagnitude = magnitude; + while (board[ahead - localMagnitude] != null) { + localMagnitude++; + }; + positiveNewPosition = ahead - localMagnitude; + board[ahead] = null; + }; + + // Simultaneous movement + if (aheadShip) { + board[positiveNewPosition] = aheadShip; + }; + if (behindShip) { + board[negativeNewPosition] = behindShip; + }; + + + if (behind < 0 && ahead >= board.length) { + break; + }; + }; +}; + + +/** @internal */ +export function moveShip(board: Board, ship: GamePiece, card: FuelCard) { + let locale = shipLocation(board, ship); + if (locale == "singularity") { + let newPos = card.magnitude; + + // Used a purple card for some reason... + if (newPos < 0) { return }; + board.path[card.magnitude] = ship; + } + else if (locale == "path") { + let oldPos = board.path.indexOf(ship); + let direction = determineDirection(board, oldPos) + let newPos = oldPos * direction * card.magnitude; + + if (newPos < 0) { + board.singularity.push(ship); + board.path[oldPos] = null; + } else if (newPos >= board.path.length) { + board.warpgate.push(ship); + board.path[oldPos] = null; + } else { + // Drift the ship + while (board.path[newPos] != null) { + newPos += direction; + }; + board.path[oldPos] = null; + board.path[newPos] = ship; + }; + }; +}; + + +/** + * Applies a card to the board as it was played by the ship. This modifies the + * board in-place, if wanting to not modify the board in place, pass a deep-copy + * of the board into the function. + * + * @param board The game board + * @param ship The ship that is playing the card + * @param card The card that is being played + */ +export function processCard(board: Board, ship: GamePiece, card: FuelCard) { + if (card.type == "movement") { + moveShip(board, ship, card); + } else { + tractorBeam(board, ship, card); + }; +}; \ No newline at end of file