Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

30 add more and better errors #32

Merged
merged 2 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions src/Board/BoardBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import Ship, { GRAPHICAL_TYPES, PLAY_TYPES } from './Ship.js';

/**
* The underlying Board class. For use as a preset, supply width and height. For use as a puzzle, supply preset and either solution or verticalCount, horizontalCount, and runs.
* @param {number} width - Width in squares
* @param {number} height - Height in squares
* @param {number} [width] - Width in squares
* @param {number} [height] - Height in squares
* @param {BoardBuilder} [preset] - Pre-existing ships
* @param {BoardBuilder} [solution] - Ending board (leave undefined if using vert/hoz count and runs)
* @param {number[]} [columnCounts] - Number of ships in each column (left to right)
Expand All @@ -29,6 +29,8 @@ export default class BoardBuilder {
// // check viability
// } -TODO

if (preset && (preset.width !== width || preset.height !== height)) throw new Error(`Preset should be the same size as the new board. Expected (${width}, ${height}), received (${preset.width}, ${preset.height})`);

this.width = width || preset?.width || 4;
this.height = height || preset?.height || 4;

Expand Down Expand Up @@ -197,15 +199,18 @@ export default class BoardBuilder {
* Count unkown and ship squares in a column
* @param {number} x - The x position of the column
* @returns {number[]} [#ships, #unkown]
* @throws {RangeError} If x is outside of the board. Should be between 0 and this.width - 1
*/
countCol (x) {
if (x > this.width - 1) throw new RangeError(`x (${x}) is outside of the board (min: 0, max: ${this.width - 1})`);

const counts = [0, 0];

for (let y = 0; y < this.height; y++) {
const ship = this.getShip([x, y]);

if (ship.playType === PLAY_TYPES.SHIP) counts[0]++;
if (ship.playType === PLAY_TYPES.UKNOWN) counts[1]++;
if (ship.playType === PLAY_TYPES.UNKNOWN) counts[1]++;
}

return counts;
Expand All @@ -215,15 +220,18 @@ export default class BoardBuilder {
* Count unkown and ship squares in a row
* @param {number} y - The row index (starts at 0)
* @returns {number[]} [#ships, #unkown]
* @throws {RangeError} If y is outside of the board. Should be between 0 and this.height - 1
*/
countRow (y) {
if (y > this.height - 1) throw new RangeError(`y (${y}) is outside of the board (min: 0, max: ${this.height - 1})`);

const counts = [0, 0];

for (let x = 0; x < this.width; x++) {
const ship = this.getShip([x, y]);

if (ship.playType === PLAY_TYPES.SHIP) counts[0]++;
if (ship.playType === PLAY_TYPES.UKNOWN) counts[1]++;
if (ship.playType === PLAY_TYPES.UNKNOWN) counts[1]++;
}

return counts;
Expand Down Expand Up @@ -328,8 +336,11 @@ export default class BoardBuilder {
* @param {boolean} [onlyCountComplete] - Only count runs that start and end with an end ship (eg. up, down, left, right). Defaults to false
* @param {boolean} [onlyCountShips] - don't include unknown squares in the count. Defaults to false
* @returns {Run[]} An array with the all the row's runs within
* @throws {RangeError} If y is outside of the board
*/
getRowRuns (y, onlyCountComplete, onlyCountShips) {
if (y > this.height - 1) throw new RangeError(`y (${y}) is outside of the board (min: 0, max: ${this.height - 1})`);

const runs = [];
let run = [];

Expand Down Expand Up @@ -377,8 +388,11 @@ export default class BoardBuilder {
* @param {boolean} [onlyCountComplete] - Only count runs that start and end with an end ship (eg. up, down, left, right)
* @param {boolean} [onlyCountShips] -- don't include unknown squares in the count
* @returns {Run[]} An array with the all the column's runs within
* @throws {RangeError} If x is outside of the board
*/
getColumnRuns (x, onlyCountComplete, onlyCountShips) {
if (x > this.width - 1) throw new RangeError(`x (${x}) must be within the board (min: 0, max: ${this.width - 1})`);

const runs = [];
let run = [];

Expand Down Expand Up @@ -444,16 +458,18 @@ export default class BoardBuilder {
* Converts a set of coordinates to an index
* @param {number[]} coordinates - An array starting at 0 as [x, y]
* @returns {number} The index
* @throws {RangeError} If coordinates are not within the board
* @throws {TypeError} If coordinates are not integers
*/
coordinatesToIndex (coordinates) {
const [x, y] = coordinates;

if (x < 0 || x > this.width - 1 || y < 0 || y > this.height - 1) {
throw new Error('Invalid input: coordinates must not exceed board dimensions');
throw new RangeError(`coordinates (${x}, ${y}) must be within board`);
}

if (!Number.isInteger(x) || !Number.isInteger(y)) {
throw new Error('Invalid input: coordinates must be integers');
throw new TypeError(`coordinates must be integers (are ${typeof x} and ${typeof y})`);
}

// paranthesis for legability
Expand All @@ -464,11 +480,12 @@ export default class BoardBuilder {
* Converts an index to a set of coordinates
* @param {number} index - The index
* @returns {number[]} An array starting at 0 as [x, y]
* @throws {RangeError} If coordinates are not within the board
* @throws {TypeError} If coordinates are not integers
*/
indexToCoordinates (index) {
if (!Number.isInteger(index)) {
throw new Error('Invalid input: coordinates must be integers');
}
if (index < 0 || index > this.width * this.height - 1) throw new RangeError(`index (${index}) must be within the board (min: 0, max: ${this.width * this.height - 1})`);
if (!Number.isInteger(index)) throw new TypeError(`index must be an integer (is ${typeof index})`);

return [index % this.width, Math.floor(index / this.width)];
}
Expand All @@ -477,17 +494,29 @@ export default class BoardBuilder {
* Converts a position (coordinates or index) into an index
* @param {number[] | number} position - An index or array starting at 0 as [x, y]
* @returns {number} The index
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
positionToIndex (position) {
if (typeof position === 'number' && position >= 0) return position;
if (Array.isArray(position)) return this.coordinatesToIndex(position);
throw new Error(`Invalid input: position must be an index or array of coordinates. Received: (${typeof position}) ${position}`);
if (typeof position === 'number') {
if (position < 0 || position > this.width * this.height - 1) throw new RangeError(`index (${position}) must be within the board (min: 0, max: ${this.width * this.height - 1})`);
return position;
}

if (Array.isArray(position) && position.length === 2) {
if (position[0] < 0 || position[0] > this.width - 1 || position[1] < 0 || position[1] > this.height - 1) throw new RangeError(`coordinates (${position[0]}, ${position[1]}) must be within the board`);
return this.coordinatesToIndex(position);
}

throw new TypeError(`position (${position}) must be an index or array of coordinates (is ${typeof position})`);
}

/**
* Get the ship at a position
* @param {number[] | number} position - An index or array starting at 0 as [x, y]
* @returns {Ship} The ship
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
getShip (position) {
const index = this.positionToIndex(position);
Expand All @@ -500,6 +529,8 @@ export default class BoardBuilder {
* @param {Ship|number} value - The ship object or type
* @param {boolean} [pinned] - Should updateGraphicalTypes ignore the ship (only works if value is a ship type)
* @returns {BoardBuilder} this
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
setShip (position, value, pinned) {
const index = this.positionToIndex(position);
Expand All @@ -525,9 +556,11 @@ export default class BoardBuilder {
* @param {Ship|number} value - The ship object or type
* @param {boolean} [pinned] - Should updateGraphicalTypes ignore the ship (only works if value is a ship type)
* @returns {boolean} True if the ship was set, false if not
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
softSetShip (position, value, pinned) {
if (this.getShip(position).playType !== PLAY_TYPES.UKNOWN) return false;
if (this.getShip(position).playType !== PLAY_TYPES.UNKNOWN) return false;

this.setShip(position, value, pinned);
return true;
Expand All @@ -537,7 +570,9 @@ export default class BoardBuilder {
* Converts a relative position to an absolute index
* @param {number[]|number} position - An index or array starting at 0 as [x, y]
* @param {number} relativePosition - The relative position
* @returns {number} The absolute index
* @returns {number|null} The absolute index or null if the square would be outside of the board
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
relativePositionToIndex (position, relativePosition) {
const index = this.positionToIndex(position);
Expand All @@ -560,6 +595,8 @@ export default class BoardBuilder {
* @param {number[] | number} basePosition - An index or array starting at 0 as [x, y]
* @param {number} relativePosition - The index relative to position
* @returns {Ship} The relative ship
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
getRelativeShip (basePosition, relativePosition) {
const index = this.relativePositionToIndex(basePosition, relativePosition);
Expand All @@ -573,6 +610,8 @@ export default class BoardBuilder {
* @param {Ship|number} value - The ship object or type
* @param {boolean} [pinned] - Should updateGraphicalTypes ignore the ship (only if value is a ship type)
* @returns {BoardBuilder|undefined} this
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
setRelativeShip (position, relativePosition, value, pinned) {
const index = this.relativePositionToIndex(position, relativePosition);
Expand All @@ -588,6 +627,8 @@ export default class BoardBuilder {
* @param {number | number[]} position - An index or array starting at 0 as [x, y]
* @param {number} [except] - A relative position to set to a ship instead of water
* @returns {BoardBuilder} this
* @throws {RangeError} If position is not within the board
* @throws {TypeError} If position is not an index (integer) or array of coordinates
*/
setCardinalShips (position, except) {
for (const relativePosition in RELATIVE_POSITIONS) {
Expand Down Expand Up @@ -631,7 +672,7 @@ export default class BoardBuilder {
floodColumn (column, type) {
for (let y = 0; y < this.height; y++) {
const square = this.getShip([column, y]);
if (square.playType === PLAY_TYPES.UKNOWN) this.setShip([column, y], type ?? PLAY_TYPES.WATER);
if (square.playType === PLAY_TYPES.UNKNOWN) this.setShip([column, y], type ?? PLAY_TYPES.WATER);
}

return this;
Expand All @@ -646,7 +687,7 @@ export default class BoardBuilder {
floodRow (row, type) {
for (let x = 0; x < this.width; x++) {
const square = this.getShip([x, row]);
if (square.playType === PLAY_TYPES.UKNOWN) this.setShip([x, row], type ?? PLAY_TYPES.WATER);
if (square.playType === PLAY_TYPES.UNKNOWN) this.setShip([x, row], type ?? PLAY_TYPES.WATER);
}

return this;
Expand Down Expand Up @@ -693,7 +734,7 @@ function createBoardState (width, height, preset) {
const ship = preset.getShip(i);
out.push(new Ship(ship.graphicalType, ship.pinned));
} else {
out.push(new Ship(PLAY_TYPES.UKNOWN));
out.push(new Ship(PLAY_TYPES.UNKNOWN));
}
}

Expand Down
20 changes: 15 additions & 5 deletions src/Board/BoardBuilder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ test('pre-existing ships', () => {
});

test('coordinatesToIndex', () => {
expect(() => { new BoardBuilder().coordinatesToIndex([4, 6]); }).toThrow('Invalid input');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4, 6.2]); }).toThrow('Invalid input');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4.6, 6.2]); }).toThrow('Invalid input');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4.6, 6]); }).toThrow('Invalid input');
expect(() => { new BoardBuilder().coordinatesToIndex([4, 6]); }).toThrow('must be within board');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4, 6.2]); }).toThrow('must be integers');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4.6, 6.2]); }).toThrow('must be integers');
expect(() => { new BoardBuilder(8, 8).coordinatesToIndex([4.6, 6]); }).toThrow('must be integers');
expect(new BoardBuilder(8, 8).coordinatesToIndex([4, 6])).toBe(52);
expect(new BoardBuilder(4, 4).coordinatesToIndex([0, 1])).toBe(4);
});
Expand Down Expand Up @@ -49,7 +49,7 @@ test('setShip', async () => {

test('getShip', () => {
const board = new BoardBuilder(4, 4);
const ship = new Ship(PLAY_TYPES.UKNOWN);
const ship = new Ship(PLAY_TYPES.UNKNOWN);

expect(board.getShip([1, 2])).toEqual(ship);
expect(board.getShip(0)).toEqual(ship);
Expand Down Expand Up @@ -205,3 +205,13 @@ test('count runs left', () => {

expect(counts).toEqual(expectedCounts);
});

test('presets', () => {
const board1 = new BoardBuilder(15, 15, board, undefined, board.columnCounts, board.rowCounts, board.runs);

expect(board1.getShip([0, 0]).equals(new Ship(PLAY_TYPES.WATER))).toBeTruthy();
expect(() => {
// eslint-disable-next-line no-new
new BoardBuilder(15, 16, board);
}).toThrow('same size as the new board');
});
21 changes: 12 additions & 9 deletions src/Board/Ship.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export default class Ship {

toString () {
switch (this.graphicalType) {
case GRAPHICAL_TYPES.UKNOWN:
return 'Uknown';
case GRAPHICAL_TYPES.UNKNOWN:
return 'Unknown';
case GRAPHICAL_TYPES.WATER:
return 'Water';
case GRAPHICAL_TYPES.SHIP:
Expand Down Expand Up @@ -48,9 +48,10 @@ export default class Ship {
* Set the play type of the ship
* @param {number} newType - The type to change it to
* @returns {Ship} this
* @throws {TypeError} If newType is not a play type
*/
setPlayType (newType) {
if (!Object.values(PLAY_TYPES).includes(newType)) throw new Error('Invalid input: newType must be a play type');
if (!Object.values(PLAY_TYPES).includes(newType)) throw new TypeError(`newType must be a play type (received: ${newType})`);

if (this.graphicalType < GRAPHICAL_TYPES.SHIP || newType < GRAPHICAL_TYPES.SHIP) this.setGraphicalType(newType);

Expand All @@ -62,9 +63,10 @@ export default class Ship {
* Set the graphical type of the ship
* @param {number} newType - The type to change it to
* @returns {Ship} this
* @throws {TypeError} If newType is not a graphical type
*/
setGraphicalType (newType) {
if (!Object.values(GRAPHICAL_TYPES).includes(newType)) throw new Error('Invalid input: newType must be a graphical type');
if (!Object.values(GRAPHICAL_TYPES).includes(newType)) throw new TypeError(`newType must be a graphical type (received: ${newType})`);

if (newType <= PLAY_TYPES.SHIP) this.playType = newType;
else if (newType > PLAY_TYPES.SHIP) this.playType = PLAY_TYPES.SHIP;
Expand Down Expand Up @@ -135,13 +137,14 @@ export default class Ship {
}

static isUnkown (squares) {
return Ship.isPlayType(squares, PLAY_TYPES.UKNOWN);
return Ship.isPlayType(squares, PLAY_TYPES.UNKNOWN);
}

/**
* Convert a graphical type to its coresponding relative position
* Convert a graphical type to its corresponding relative position
* @param {number} graphicalType - The graphical type to convert
* @returns {number} The coresponding relative position
* @returns {number} The corresponding relative position
* @throws If there's no single corresponding relative position
*/
static graphicalTypeToRelativePosition (graphicalType) {
switch (graphicalType) {
Expand All @@ -165,7 +168,7 @@ export default class Ship {
*/
export const PLAY_TYPES = {
// playable/basics
UKNOWN: 0,
UNKNOWN: 0,
WATER: 1,
SHIP: 2
};
Expand All @@ -175,7 +178,7 @@ export const PLAY_TYPES = {
* @constant
*/
export const GRAPHICAL_TYPES = {
UKNOWN: 0,
UNKNOWN: 0,
WATER: 1,

// ships
Expand Down
18 changes: 6 additions & 12 deletions src/Board/Ship.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ import Ship, { GRAPHICAL_TYPES, PLAY_TYPES } from './Ship';

test('set types', () => {
// play types
expect(new Ship(PLAY_TYPES.UKNOWN).setPlayType(PLAY_TYPES.WATER).playType).toBe(PLAY_TYPES.WATER);
expect(() => {
new Ship(PLAY_TYPES.UKNOWN).setPlayType(GRAPHICAL_TYPES.UP);
}).toThrow('Invalid input');
expect(new Ship(PLAY_TYPES.UNKNOWN).setPlayType(PLAY_TYPES.WATER).playType).toBe(PLAY_TYPES.WATER);
expect(() => { new Ship(PLAY_TYPES.UNKNOWN).setPlayType(GRAPHICAL_TYPES.UP); }).toThrow('must be a play type');

// graphical types
expect(new Ship(PLAY_TYPES.UKNOWN).setGraphicalType(GRAPHICAL_TYPES.LEFT).graphicalType).toBe(GRAPHICAL_TYPES.LEFT);
expect(() => {
new Ship(PLAY_TYPES.UKNOWN).setGraphicalType(10);
}).toThrow('Invalid input');
expect(new Ship(PLAY_TYPES.UNKNOWN).setGraphicalType(GRAPHICAL_TYPES.LEFT).graphicalType).toBe(GRAPHICAL_TYPES.LEFT);
expect(() => { new Ship(PLAY_TYPES.UNKNOWN).setGraphicalType(10); }).toThrow('must be a graphical type');
});

test('set types propogation', () => {
const ship = new Ship(PLAY_TYPES.UKNOWN);
const ship = new Ship(PLAY_TYPES.UNKNOWN);

ship.setPlayType(PLAY_TYPES.WATER);
expect(ship.graphicalType).toBe(GRAPHICAL_TYPES.WATER);
Expand All @@ -31,10 +27,8 @@ test('set types propogation', () => {
expect(ship.playType).toBe(PLAY_TYPES.WATER);
});

// this really shouldn't need to be here (but it does need to be here)
// any time you make a ship with the ship type it removes the playtype for some reason
test('doesn\'t delete attributes', () => {
expect(new Ship(PLAY_TYPES.SHIP).playType).toBeDefined();
expect(new Ship(PLAY_TYPES.WATER).playType).toBeDefined();
expect(new Ship(PLAY_TYPES.UKNOWN).playType).toBeDefined();
expect(new Ship(PLAY_TYPES.UNKNOWN).playType).toBeDefined();
});
Loading