advent-of-code-client
Version:
A NodeJS client for fetching inputs, running puzzle challenges and submitting answers to Advent Of Code directly from your JavaScript code.
227 lines (203 loc) • 7.29 kB
text/typescript
import { getYear } from 'date-fns';
import CacheConf from 'cache-conf';
import emojic from 'emojic';
import { getInput, postAnswer } from './util/api';
import logger from './util/logger';
import waitForUserInput from './util/waitForUserInput';
import type {
CacheKeyParams,
Cache,
Config,
PartFn,
Result,
TransformFn
} from './AocClient.types';
const getCacheKey = ({ year, day, token, part }: CacheKeyParams) =>
`${year}:${day}:${token}:${part}`;
/**
* A class that handles fetching input from and submitting answers to Advent Of Code.
* Each instance of the class corresponds to a puzzle for a specific day and year based on the configuration.
*/
class AocClient {
private config: Config;
private cache: Cache;
private transform: TransformFn;
/**
* @param {object} config
*/
constructor({ year, day, token, useCache = true, debug = false }: Config) {
if (
!year ||
Number.isNaN(year) ||
year < 2015 ||
year > getYear(new Date())
) {
throw new Error(
'Missing or invalid year option, year must be a number between 2015 and current year'
);
}
if (!day || Number.isNaN(day) || day < 1 || day > 25) {
throw new Error(
'Missing or invalid day option, day must be a number between 1 and 25'
);
}
if (!token || typeof token !== 'string') {
throw new Error('Missing or invalid token option');
}
if (typeof useCache !== 'boolean') {
throw new Error('Invalid useCache option, useCache can only be boolean');
}
if (typeof debug !== 'boolean') {
throw new Error('Invalid debug option, debug can only be boolean');
}
this.config = {
year,
day,
token,
useCache
};
if (debug) {
globalThis.aocDebug = true;
}
this.cache = new CacheConf();
this.transform = null;
}
private _hasCompletedPart(part: number) {
const cacheKey = getCacheKey({ ...this.config, part });
return this.cache.get(cacheKey) === true;
}
private _markCompletedPart(part: number) {
const cacheKey = getCacheKey({ ...this.config, part });
this.cache.set(cacheKey, true);
}
/**
* Get the input for the puzzle.
* @return the puzzle input. If a transform function has been set using the ´setInputTransform´ method it will return the transformed input, otherwise the raw input is returned.
*/
async getInput() {
logger.log('Fetching input...');
const input = await getInput(this.config, this.cache);
const trimmedInput = input.trim();
return this.transform ? this.transform(trimmedInput) : trimmedInput;
}
/**
* Submit puzzle answer for a specific part of the puzzle. If the part of the puzzle has already been completed, it will not be submitted again.
* @param {number} part - the part of the puzzle that the answer is for (should be 1 or 2).
* @param {number | string} answer - the answer to the puzzle.
* @return {boolean} true if the answer was correct, false otherwise.
*/
async submit(part: number, answer: Result): Promise<boolean> {
if (part !== 1 && part !== 2) {
return Promise.reject(new Error('Part must be either 1 or 2'));
}
logger.log(`Submitting part ${part}...`);
if (part === 1 && this._hasCompletedPart(1)) {
logger.success('Part 1 already completed');
return Promise.resolve(true);
}
if (part === 2 && this._hasCompletedPart(2)) {
logger.success(
'Part 2 already completed successfully, continue with next puzzle'
);
return Promise.resolve(true);
}
const { correct } = await postAnswer(
{ part, answer },
this.config,
this.cache
);
if (correct) {
this._markCompletedPart(part);
}
const resultLogger = correct ? logger.success : logger.fail;
resultLogger(`Result: ${answer}`);
if (part === 2 && correct) {
console.log();
logger.log("All done! Great job, here's a cookie", emojic.cookie);
}
return correct;
}
/**
* Run the puzzle parts (it can run either only part 1 or part 1 and 2) and submit the answers. A part that has previously been completed successfully will not run again.
* @param {array} parts - an array with length of either 1 or 2. Each element in the array must be a function that takes the puzzle input and returns the calculated puzzle answer. The first element in the array corresponds to part 1 of the puzzle, and the second element (if specified) corresponds to part 2 of the puzzle.
* @param {boolean} autoSubmit - when true the answers for each part will be submitted to Advent Of Code automatically, otherwise each answer will require confirmation before it will be submitted.
*/
async run(
parts: [part1: PartFn] | [part1: PartFn, part2: PartFn],
autoSubmit = false
) {
if (!parts || !parts.length || parts.length > 2) {
return Promise.reject(
new Error('Parts must be an array with length between 1 and 2')
);
}
if (
typeof parts[0] !== 'function' ||
(parts[1] !== undefined && typeof parts[1] !== 'function')
) {
return Promise.reject(
new Error('All elements in the parts array must be of type function')
);
}
if (this._hasCompletedPart(1) && this._hasCompletedPart(2)) {
logger.log(
'Both parts already completed successfully, continue with next puzzle',
emojic.star,
emojic.star
);
return Promise.resolve();
}
if (
parts.length === 1 &&
this._hasCompletedPart(1) &&
!this._hasCompletedPart(2)
) {
logger.log(
'Part 1 already completed successfully, continue with part 2',
emojic.star
);
return Promise.resolve();
}
const input = await this.getInput();
const results: Array<Result> = [undefined, undefined];
if (!this._hasCompletedPart(1)) {
results[0] = parts[0](input);
} else {
logger.success('Part 1 already completed');
}
if (!this._hasCompletedPart(2) && parts.length === 2) {
results[1] = parts[1](input);
}
if (autoSubmit) {
logger.log('Submitting answers automatically');
if (results[0] !== undefined) {
await this.submit(1, results[0]);
}
if (results[1] !== undefined) {
await this.submit(2, results[1]);
}
return Promise.resolve();
}
if (results[0] !== undefined) {
logger.log('Your result from part 1 is', results[0]);
logger.log('Do you want to submit it? (Y/N):');
const userInput = await waitForUserInput();
if (userInput.toLowerCase() !== 'y') return Promise.resolve();
await this.submit(1, results[0]);
}
if (results[1] !== undefined) {
logger.log('Your result from part 2 is', results[1]);
logger.log('Do you want to submit it? (Y/N):');
const userInput = await waitForUserInput();
if (userInput.toLowerCase() !== 'y') return Promise.resolve();
await this.submit(2, results[1]);
}
return Promise.resolve();
}
setInputTransform(transform: TransformFn) {
if (typeof transform !== 'function')
throw new Error('transform must be a function');
this.transform = transform;
}
}
export default AocClient;