Skip to content

Instantly share code, notes, and snippets.

@bluepichu
Last active December 9, 2021 05:23
Show Gist options
  • Save bluepichu/9542fee5ae58f506e7952f2d25b3b108 to your computer and use it in GitHub Desktop.
Save bluepichu/9542fee5ae58f506e7952f2d25b3b108 to your computer and use it in GitHub Desktop.
import { readFile, writeFile } from "fs/promises";
import { join as pathJoin } from "path";
import { List } from "immutable";
import fetch from "node-fetch";
import * as readline from "readline";
const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout });
const session = (await readFile(pathJoin(process.env.HOME!, ".advent_cookie"))).toString().trim();
export const int = (x: string): number => parseInt(x, 10);
export const num = (x: string): number => parseFloat(x);
export const str = (x: any): string => x.toString();
export const bool = (x: any): boolean => !!x;
export const arr = <T>(n: number, fn: (i: number) => T) => Array.from(new Array(n), (_, i) => fn(i));
export const print = console.log;
export interface InputOptions {
day: number;
year?: number;
strip?: boolean;
}
export class Input {
constructor(
private content: string
) {}
public static async get(options: InputOptions): Promise<Input> {
const day = options.day;
const year = options.year ?? 2021;
const useSample = process.env.ADVENT_SAMPLE === "1";
const file = useSample ? "sample.txt" : "input.txt";
const url = useSample ? undefined : `https://adventofcode.com/${options.year}/day/${options.day}/input`;
const strip = options.strip ?? true;
const preprocess = (data: string) => strip ? data.trimRight() : data;
try {
const buffer = await readFile(file);
print(`Using input from ${file}`);
return new Input(preprocess(buffer.toString()));
} catch (error) {
if (
error instanceof Error
&& "code" in error
&& (error as any).code === "ENOENT"
&& url !== undefined
) {
print("Downloading input...");
const response = await fetch(`https://adventofcode.com/${year}/day/${day}/input`, {
headers: {
cookie: `session=${session}`,
},
});
if (response.status !== 200) {
print(await response.text());
throw new Error(`Failed to fetch input: ${response.status}`);
}
const content = await response.text();
await writeFile(file, content);
return new Input(preprocess(content));
}
throw error;
}
}
public all(): string {
return this.content;
}
public lines(): string[] {
return this.content.split("\n");
}
public forLines<T>(fn: (input: Input) => T): T[] {
return this.lines().map((line) => fn(new Input(line)));
}
public tokens(separator: RegExp = /[\s\n]+/g): string[] {
return this.content.split(separator);
}
public lineTokens(lineSeparator: RegExp = /\n/g, tokenSeparator: RegExp = /[\s]+/g): string[][] {
return this.content.split(lineSeparator).map((line) => line.split(tokenSeparator));
}
public ints(): number[] {
return this.tokens().map(int);
}
public nums(): number[] {
return this.tokens().map(num);
}
}
export type Point = [number, number];
export type Point3D = [number, number, number];
export const GridDirections = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1],
];
export const GridDirectionsWithoutDiagonals = [
[-1, 0],
[0, -1],
[1, 0],
[0, 1],
];
export function* neighbors(point: Point, includeDiagonal: boolean = true): Generator<Point> {
const directions = includeDiagonal ? GridDirections : GridDirectionsWithoutDiagonals;
for (let [di, dj] of directions) {
yield [point[0] + di, point[1] + dj];
}
}
export async function Advent(options: InputOptions) {
const input = await Input.get(options);
const submit = async (answer: number | string, level: 1 | 2 = 1) => {
await new Promise<void>((resolve) => {
readlineInterface.question(`Submit answer \x1b[36m${answer}\x1b[0m for \x1b[35mpart ${level}\x1b[0m? \x1b[90m(Y to submit)\x1b[0m `, (response) => {
if (response.toLowerCase() === "y") {
readlineInterface.close();
resolve();
} else {
print("\x1b[31mRefusing to submit\x1b[0m");
process.exit(1);
}
});
});
print("\x1b[34mSubmitting...\x1b[0m");
const response = await fetch(`https://adventofcode.com/${options.year ?? 2021}/day/${options.day}/answer`, {
method: "POST",
headers: {
cookie: `session=${session}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: `level=${level}&answer=${encodeURIComponent(answer)}`,
});
const text = await response.text();
if (response.status !== 200) {
print(await response.text());
throw new Error(`Failed to submit answer: ${response.status}`);
}
if (text.includes("That's not the right answer")) {
print(text);
print("\x1b[31mAnswer incorrect!\x1b[0m");
process.exit(1);
} else if (text.includes("You gave an answer too recently")) {
print(`\x1b[33mWait for the timeout (${/You have (.*) left to wait/.exec(text)![1]})\x1b[0m`);
process.exit(1);
} else if (text.includes("That's the right answer!")) {
print("\x1b[32mAnswer correct!\x1b[0m");
process.exit(0);
} else if (text.includes("You don't seem to be solving the right level.")) {
print("\x1b[31mWrong level! (Already solved?)\x1b[0m");
process.exit(1);
} else {
print(text);
print(`\x1b[33mUnknown outcome!\x1b[0m`);
process.exit(1);
}
};
return {
input,
submit,
compute: async (fn: (input: Input) => Promise<number | string>, level: 1 | 2 = 1) => {
const answer = await fn(input);
await submit(answer, level);
},
computeCheck: async (fn: (input: Input) => AsyncGenerator<number | string>, level: 1 | 2 = 1) => {
let answer: number | string | undefined;
let ok = true;
for await (const a of fn(input)) {
if (answer === undefined) {
answer = a;
} else {
if (answer !== a) {
print(`\x1b[31mGot mismatched answer: ${answer} ≠ ${a}\x1b[0m`);
ok = false;
}
}
}
if (answer === undefined) {
print("\x1b[31mDidn't get an answer\x1b[0m");
process.exit(1);
} else if (!ok) {
print("\x1b[31mRefusing to submit due to mismatched answers\x1b[0m");
process.exit(1);
} else {
await submit(answer, level);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment