Skip to content
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
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
. "$(dirname -- "$0")/_/husky.sh"

bash <<EOF
set -e
TO_PRETTIFY="$(git diff --diff-filter=ACMTUXB --name-only --staged | grep -E -e '\.[jt]sx?$' -e '\.md$' -e '\.html$' -e '\.json$' || :) )"
if [[ -n "$TO_PRETTIFY" ]]; then npm run prettify $TO_PRETTIFY; fi
npm test
Expand Down
2 changes: 1 addition & 1 deletion src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,6 @@ export async function evaluate(
}
return {
success: success,
output: formatValue(value!, mode),
output: formatValue(value, mode),
};
}
186 changes: 117 additions & 69 deletions src/globals/prototypes.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
import { sample, useRNG } from '@/utils/random';

import type { TranslationFn } from '@/i18n/types';
import type { ArrayAtom } from '@/types/common';
import type { RNGSource } from '@/utils/random';

declare global {
interface Array<T> {
access<V = ArrayAtom<T>>(pos: number[]): V;
count(): Record<T & (string | number), number>;
count(map: true): Map<T, number>;
filterMap<X>(cb: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined;
list($T?: TranslationFn | string): string;
random(rng?: RNGSource): T;
sample(amount: number, rng?: RNGSource): T[];
remove(...toRemove: T[]): T[];
sample(amount: number, rng?: RNGSource): T[];
shuffle(rng?: RNGSource): T[];
filterMap<X>(cb: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined;
unique(): T[];
list($T?: TranslationFn | string): string;
sortBy(getSort: (term: T, thisArray: T[]) => unknown, dir?: 'asc' | 'desc'): T[];
space<S = unknown>(spacer: S): (T | S)[];
count(): Record<T & (string | number), number>;
sum(): T;
unique(): T[];
}
interface ReadonlyArray<T> {
random(rng?: RNGSource): T;
sample(amount: number, rng?: RNGSource): T[];
access<V = ArrayAtom<T>>(pos: number[]): V;
count(): Record<T & (string | number), number>;
count(map: true): Map<T, number>;
filterMap<X>(cb: (element: T, index: number, thisArray: T[]) => X | undefined): X | undefined;
unique(): T[];
list($T?: TranslationFn): string;
random(rng?: RNGSource): T;
sample(amount: number, rng?: RNGSource): T[];
space<S = unknown>(spacer: S): (T | S)[];
count(): Record<T & string, number>;
sum(): T;
unique(): T[];
}

interface String {
lazySplit(match: string | RegExp, cases: number): string[];
gsub(match: RegExp, replace: string | ((arg: string, ...captures: string[]) => string)): string;
lazySplit(match: string | RegExp, cases: number): string[];
}

interface Number {
Expand All @@ -36,6 +44,37 @@ declare global {
}

Object.defineProperties(Array.prototype, {
access: {
enumerable: false,
writable: false,
configurable: false,
value: function <T, V = ArrayAtom<T>>(this: T[], point: number[]): V {
// eslint-disable-next-line -- Consumer-side responsibility for type safety
return point.reduce<any>((arr, index) => arr[index], this);
},
},
count: {
enumerable: false,
writable: false,
configurable: false,
value: function <T extends string | number | symbol = unknown & (string | number)>(
this: T[],
map?: boolean
): Record<T & string, number> | Map<T, number> {
if (map) {
return this.reduce<Map<T, number>>((out, term) => {
if (!out.has(term)) out.set(term, 1);
else out.set(term, out.get(term)! + 1);
return out;
}, new Map());
}
return (this as string[]).reduce<Record<string, number>>((out, term) => {
out[term] ??= 0;
out[term]++;
return out;
}, {});
},
},
filterMap: {
enumerable: false,
writable: false,
Expand All @@ -51,17 +90,18 @@ Object.defineProperties(Array.prototype, {
}
},
},
remove: {
list: {
enumerable: false,
writable: false,
configurable: false,
value: function <T = unknown>(this: T[], ...terms: T[]): boolean {
let out = true;
terms.forEach(term => {
if (this.indexOf(term) >= 0) this.splice(this.indexOf(term), 1);
else out = false;
});
return out;
value: function <T extends string | number>(this: T[], $T?: TranslationFn | string): string {
const conjunction = typeof $T === 'string' ? $T : ($T?.('GRAMMAR.AND') ?? 'and');
if (this.length === 0) return '';
if (this.length === 1) return this.toString();
if (this.length === 2) return this.map(term => term.toString()).join(` ${conjunction} `);
return `${this.slice(0, -1)
.map(term => term.toString())
.join(', ')}, ${conjunction} ${this.at(-1)!.toString()}`;
},
},
random: {
Expand All @@ -72,11 +112,24 @@ Object.defineProperties(Array.prototype, {
return this[sample(this.length, useRNG(rng))];
},
},
remove: {
enumerable: false,
writable: false,
configurable: false,
value: function <T = unknown>(this: T[], ...terms: T[]): boolean {
let out = true;
terms.forEach(term => {
if (this.indexOf(term) >= 0) this.splice(this.indexOf(term), 1);
else out = false;
});
return out;
},
},
sample: {
enumerable: false,
writable: false,
configurable: false,
value: function T<T = unknown>(this: T[], amount: number, rng?: RNGSource): T[] {
value: function T<T>(this: T[], amount: number, rng?: RNGSource): T[] {
const RNG = useRNG(rng);
const sample = Array.from(this),
out: T[] = [];
Expand All @@ -93,7 +146,7 @@ Object.defineProperties(Array.prototype, {
enumerable: false,
writable: false,
configurable: false,
value: function <T = unknown>(this: T[], rng?: RNGSource): T[] {
value: function <T>(this: T[], rng?: RNGSource): T[] {
const RNG = useRNG(rng);
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(RNG() * (i + 1));
Expand All @@ -102,41 +155,24 @@ Object.defineProperties(Array.prototype, {
return Array.from(this);
},
},
unique: {
enumerable: false,
writable: false,
configurable: false,
value: function <T = unknown>(this: T[]): T[] {
const output: T[] = [];
const cache = new Set();
for (let i = 0; i < this.length; i++) {
if (!cache.has(this[i])) {
cache.add(this[i]);
output.push(this[i]);
}
}
return output;
},
},
list: {
sortBy: {
enumerable: false,
writable: false,
configurable: false,
value: function <T extends string | number>(this: T[], $T?: TranslationFn | string): string {
const conjunction = typeof $T === 'string' ? $T : ($T?.('GRAMMAR.AND') ?? 'and');
if (this.length === 0) return '';
if (this.length === 1) return this.toString();
if (this.length === 2) return this.map(term => term.toString()).join(` ${conjunction} `);
return `${this.slice(0, -1)
.map(term => term.toString())
.join(', ')}, ${conjunction} ${this.at(-1)!.toString()}`;
value: function <T, W = number>(this: T[], getSort: (term: T, thisArray: T[]) => W, dir?: 'asc' | 'desc'): T[] {
const cache = this.reduce<Map<T, W>>((map, term) => {
map.set(term, getSort(term, this));
return map;
}, new Map());
// TODO: Check if this order is right
return this.sort((a, b) => ((dir === 'desc' ? cache.get(a)! > cache.get(b)! : cache.get(b)! < cache.get(a)!) ? 1 : -1));
},
},
space: {
enumerable: false,
writable: false,
configurable: false,
value: function <T = unknown, S = unknown>(this: T[], spacer: S): (T | S)[] {
value: function <T, S>(this: T[], spacer: S): (T | S)[] {
if (this.length === 0 || this.length === 1) return this;
return this.slice(1).reduce<(T | S)[]>(
(acc, term) => {
Expand All @@ -147,22 +183,49 @@ Object.defineProperties(Array.prototype, {
);
},
},
count: {
sum: {
enumerable: false,
writable: false,
configurable: false,
value: function <T extends string | number | symbol = unknown & (string | number)>(this: T[]): Record<T, number> {
const out = {} as Record<T, number>;
this.forEach(term => {
out[term] ??= 0;
out[term]++;
});
return out;
// Types here intentionally don't include string because gah
value: function (this: number[]): number {
return this.reduce((sum, term) => sum + term);
},
},
unique: {
enumerable: false,
writable: false,
configurable: false,
value: function <T>(this: T[]): T[] {
const output: T[] = [];
const cache = new Set();
for (let i = 0; i < this.length; i++) {
if (!cache.has(this[i])) {
cache.add(this[i]);
output.push(this[i]);
}
}
return output;
},
},
});

Object.defineProperties(String.prototype, {
gsub: {
enumerable: false,
writable: false,
configurable: false,
value: function (this: string, match: RegExp, replacer: string | ((substring: string, ...args: string[]) => string)): string {
let output = this.toString();
while (true) {
// TypeScript what the heck
const next = typeof replacer === 'string' ? output.replace(match, replacer) : output.replace(match, replacer);
if (next === output) break;
output = next;
}
return output;
},
},
lazySplit: {
enumerable: false,
writable: false,
Expand Down Expand Up @@ -191,21 +254,6 @@ Object.defineProperties(String.prototype, {
return out;
},
},
gsub: {
enumerable: false,
writable: false,
configurable: false,
value: function (this: string, match: RegExp, replacer: string | ((substring: string, ...args: string[]) => string)): string {
let output = this.toString();
while (true) {
// TypeScript what the heck
const next = typeof replacer === 'string' ? output.replace(match, replacer) : output.replace(match, replacer);
if (next === output) break;
output = next;
}
return output;
},
},
});

Object.defineProperties(Number.prototype, {
Expand Down
24 changes: 24 additions & 0 deletions src/i18n/english.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable max-len */

export default {
GRAMMAR: {
AND: 'and',
Expand Down Expand Up @@ -42,6 +44,7 @@ export default {
'Prayer not found. Or something like that.',
],
NOT_STARTED: 'The game has not started yet.',
CANNOT_START: 'Cannot start the game! Check the players.',
NOT_WATCHING: "You aren't watching this game, though...",
NOW_WATCHING: 'You are now watching the game of {{game}} between {{players}}.',
NO_LONGER_WATCHING: 'You are no longer watching the game of {{game}} between {{players}}.',
Expand All @@ -50,6 +53,7 @@ export default {
STASHED: 'Successfully stashed game {{id}}.',
SUB: '{{out}} has been subbed with {{in}}!',
WATCHING_NOTHING: "You don't seem to need to rejoin anything...",
WON: '{{winner}} won!',
WON_AGAINST: '{{winner}} won the game of {{game}} against {{loser}}{{ctx}}!',
WAITING: 'Waiting for you to play...',
NON_PLAYER_OR_SPEC: 'User not in players/spectators',
Expand All @@ -59,6 +63,26 @@ export default {
PRIVATE: "Psst it's your turn to play in {{game}} [{{id}}]",
PUBLIC: "{{user}} hasn't played in {{game}} [{{id}}] for {{time}}...",
},

MASTERMIND: {
ENDED: 'The game of Mastermind was ended for {{player}}.',
FAILED: '{{player}} was unable to guess {{solution}} in {{cap}} guesses.',
},
SCRABBLE: {
NO_SELECTED: 'You must select a cell to play from first. Please use the buttons!',
TILE_MISMATCH: "That move doesn't seem to line up with the tiles on the board - tried to place {{placed}} on {{actual}}.",
MISSING_LETTER: "You don't have any tiles for {{letter}}.",
INSUFFICIENT_LETTERS: 'You only have {{actual}} tiles of {{letter}} instead of the {{required}} needed.',
BAG_SIZE: 'There are {{amount}} tiles left in the bag.',
TOO_MUCH_PASSING: 'The game ended due to too many passes!',
FIRST_MOVE_CENTER: 'The first move must pass through the center of the board!',
FIRST_MOVE_MULTIPLE_TILES: 'You may not play a single tile on the first move.',
MUST_BE_CONNECTED: 'All moves in Scrabble must be connected to the rest of the tiles on the board!',
MUST_PLAY_TILES: 'Your move must play at least one tile.',
INVALID_WORD: '{{word}} is not a valid word.',
HOW_TO_BLANK:
"Hi, you've drawn a blank tile! A blank tile can be used as any letter, but the tile awards 0 points. You can type `BL[A]NK` (for example) to use the blank as an A. Other syntaxes supported are `BL(A)NK`, or adding an apostrophe after the blanked letter (eg: `BLA'NK`).",
},
},

COMMANDS: {
Expand Down
Loading