Skip to content

Commit 0f21a4d

Browse files
committed
tours: Pet Mods, automated points
1 parent 7d99755 commit 0f21a4d

1 file changed

Lines changed: 145 additions & 91 deletions

File tree

src/ps/handlers/tours.tsx

Lines changed: 145 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ import { Temporal } from '@js-temporal/polyfill';
22

33
import { PSCommands, PSPointsNonce, PSRoomConfigs } from '@/cache';
44
import { prefix } from '@/config/ps';
5-
import { bulkAddPoints } from '@/database/points';
5+
import { type BulkPointsDataInput, bulkAddPoints } from '@/database/points';
66
import { ANNOUNCEMENTS_CHANNEL, ROLES } from '@/discord/constants/servers/petmods';
77
import { getChannel } from '@/discord/loaders/channels';
88
import { IS_ENABLED } from '@/enabled';
99
import { i18n } from '@/i18n';
1010
import { TimeZone } from '@/ps/handlers/cron/constants';
1111
import getSecretFunction from '@/secrets/functions';
1212
import { toId } from '@/tools';
13+
import { Username } from '@/utils/components';
1314
import { Form } from '@/utils/components/ps';
1415
import { Logger } from '@/utils/logger';
16+
import { pluralize } from '@/utils/pluralize';
1517
import { randomString } from '@/utils/random';
1618

1719
import type { PSCommandContext } from '@/types/chat';
@@ -58,6 +60,16 @@ function inRange(time: Temporal.PlainTime, range: [Temporal.PlainTime, Temporal.
5860
return Temporal.PlainTime.compare(time, range[0]) === -1 && Temporal.PlainTime.compare(time, range[1]) === 1;
5961
}
6062
}
63+
64+
function labelPoints(data: Record<string, number>, pointsType: string): Record<string, Record<string, number>> {
65+
// TODO: Add mapValues
66+
return Object.fromEntries(Object.entries(data).map(([user, amount]) => [user, { [pointsType]: amount }]));
67+
}
68+
69+
function toBulkData(data: Record<string, Record<string, number>>): BulkPointsDataInput {
70+
return Object.fromEntries(Object.entries(data).map(([user, points]) => [user, { id: toId(user), name: user, points }]));
71+
}
72+
6173
export function tourHandler(this: Client, roomId: string, line: string, isIntro?: boolean): void {
6274
if (isIntro) return;
6375

@@ -114,109 +126,151 @@ export function tourHandler(this: Client, roomId: string, line: string, isIntro?
114126
if (e instanceof Error) Logger.errorLog(e);
115127
return;
116128
}
117-
if (roomId === 'hindi') {
118-
if (/casual|ignore|no ?points/i.test(json.format || '')) return;
119-
// The actual algorithm is secret
120-
// Nice try, though
121-
const scoringAlgo = getSecretFunction<(tourBracket: string) => Record<string, number> | null>(
122-
'hindiTourPointsAlgo',
123-
() => null
124-
);
125-
const pointsToAdd = scoringAlgo(data);
126-
if (!pointsToAdd) return;
127-
128-
const pointsType = PSRoomConfigs[roomId]?.points?.priority[0];
129-
if (!pointsType) throw new Error(`AAAAAA someone ping PartMan for ${roomId}`);
130-
bulkAddPoints(
131-
Object.fromEntries(
132-
Object.entries(pointsToAdd).map(([user, points]) => [
133-
user,
134-
{ id: toId(user), name: user, points: { [pointsType]: points } },
135-
])
136-
),
137-
roomId
138-
).then(async res => {
139-
if (!res) return;
140-
const lbCommand = PSCommands.leaderboard;
141-
const partialMessage: RecursivePartial<PSMessage> = {
142-
type: 'chat',
143-
target: room,
144-
parent: this,
145-
};
146-
const $T = i18n(); // TODO: Use language pref
147-
const partialContext: Partial<PSCommandContext> = {
148-
args: [],
149-
message: partialMessage as PSMessage,
150-
broadcastHTML: room.sendHTML.bind(room),
151-
$T,
152-
};
153-
154-
lbCommand.run(partialContext as PSCommandContext);
129+
130+
const showLeaderboard = () => {
131+
const lbCommand = PSCommands.leaderboard;
132+
const partialMessage: RecursivePartial<PSMessage> = {
133+
type: 'chat',
134+
target: room,
135+
parent: this,
136+
};
137+
const $T = i18n(); // TODO: Use language pref
138+
const partialContext: Partial<PSCommandContext> = {
139+
args: [],
140+
message: partialMessage as PSMessage,
141+
broadcastHTML: room.sendHTML.bind(room),
142+
$T,
143+
};
144+
lbCommand.run(partialContext as PSCommandContext);
145+
};
146+
147+
/** [#1, #2, #3 and #4] */
148+
const getTopFour = (): string[] => {
149+
const winners: string[] = [];
150+
const root = json.bracketData.rootNode;
151+
152+
// add first place
153+
winners.push(root.team);
154+
// add second place
155+
root.children?.forEach(child => {
156+
if (child.team !== winners[0]) winners.push(child.team);
157+
});
158+
// add runners-up
159+
root.children?.forEach(child => {
160+
if (Array.isArray(child.children))
161+
child.children.forEach(kid => {
162+
if (!winners.includes(kid.team)) winners.push(kid.team);
163+
});
155164
});
156-
}
157-
if (roomId === 'capproject') {
158-
const currentTime = Temporal.Now.instant().toZonedDateTimeISO(TimeZone.AST).toPlainTime();
159165

160-
if (
161-
inRange(currentTime, [new Temporal.PlainTime(11), new Temporal.PlainTime(13)]) ||
162-
inRange(currentTime, [new Temporal.PlainTime(23), new Temporal.PlainTime(1)])
163-
) {
164-
if (json.generator !== 'Single Elimination') return;
166+
return winners;
167+
};
165168

166-
const root = json.bracketData.rootNode;
167-
const winners: string[] = [];
168-
const pointsToAdd: Record<string, number> = {};
169+
switch (roomId) {
170+
case 'hindi': {
171+
if (/casual|ignore|no ?points/i.test(json.format || '')) return;
172+
// The actual algorithm is secret
173+
// Nice try, though
174+
const scoringAlgo = getSecretFunction<(tourBracket: string) => Record<string, number> | null>(
175+
'hindiTourPointsAlgo',
176+
() => null
177+
);
178+
const pointsToAdd = scoringAlgo(data);
179+
if (!pointsToAdd) return;
169180

170-
// add first place
171-
winners.push(root.team);
172-
// add second place
173-
root.children?.forEach(child => {
174-
if (child.team !== winners[0]) winners.push(child.team);
175-
});
176-
// add runners-up
177-
root.children?.forEach(child => {
178-
if (Array.isArray(child.children))
179-
child.children.forEach(kid => {
180-
if (!winners.includes(kid.team)) winners.push(kid.team);
181-
});
181+
const pointsType = PSRoomConfigs[roomId]?.points?.priority[0];
182+
if (!pointsType) throw new Error(`AAAAAA someone ping PartMan for ${roomId}`);
183+
bulkAddPoints(toBulkData(labelPoints(pointsToAdd, pointsType)), roomId).then(res => {
184+
if (res) showLeaderboard();
182185
});
186+
break;
187+
}
188+
189+
case 'capproject': {
190+
const currentTime = Temporal.Now.instant().toZonedDateTimeISO(TimeZone.AST).toPlainTime();
183191

184-
[3, 2, 1, 1].forEach((amt, index) => {
185-
if (winners[index]) {
186-
pointsToAdd[winners[index]] = amt;
192+
if (
193+
inRange(currentTime, [new Temporal.PlainTime(11), new Temporal.PlainTime(13)]) ||
194+
inRange(currentTime, [new Temporal.PlainTime(23), new Temporal.PlainTime(1)])
195+
) {
196+
if (json.generator !== 'Single Elimination') return;
197+
198+
const winners = getTopFour();
199+
const pointsToAdd: Record<string, number> = {};
200+
201+
[3, 2, 1, 1].forEach((amt, index) => {
202+
if (winners[index]) {
203+
pointsToAdd[winners[index]] = amt;
204+
}
205+
});
206+
207+
const pointsType = PSRoomConfigs[roomId]?.points?.types.tournight;
208+
if (!pointsType) {
209+
room.send("Hi for some reason Tour Nights don't exist, someone go poke PartMan");
210+
Logger.errorLog(new Error(`CAP room points: ${JSON.stringify(PSRoomConfigs[roomId])}`));
211+
return;
187212
}
188-
});
189213

190-
const pointsType = PSRoomConfigs.capproject?.points?.types.tournight;
214+
const nonce = randomString();
215+
PSPointsNonce[nonce] = labelPoints(pointsToAdd, pointsType.id);
216+
217+
room.sendHTML(
218+
<div className="infobox">
219+
<p>
220+
<b>{pointsType.plural}</b>
221+
{': '}
222+
{Object.entries(pointsToAdd)
223+
.map(([user, amount]) => `+${amount} ${user}`)
224+
.join(', ')}
225+
</p>
226+
<p>
227+
<Form value={`/botmsg ${this.status.username},${prefix}@${roomId} addnonce ${nonce}`}>
228+
<button>Add Points!</button>
229+
</Form>
230+
</p>
231+
</div>,
232+
{ rank: '%' }
233+
);
234+
}
235+
break;
236+
}
237+
238+
case 'petmods': {
239+
const winners = getTopFour();
240+
if (winners.length < 4) {
241+
room.sendHTML(<div className="infobox">Not adding points for this (only {winners.length} players).</div>, { rank: '%' });
242+
return;
243+
}
244+
245+
const roomConfig = PSRoomConfigs[roomId]?.points;
246+
const pointsType = roomConfig?.types[roomConfig?.priority[0]];
191247
if (!pointsType) {
192-
room.send("Hi for some reason Tour Nights don't exist, someone go poke PartMan");
193-
Logger.errorLog(new Error(`CAP room points: ${JSON.stringify(PSRoomConfigs.capproject)}`));
248+
room.send("Hi for some reason points aren't configured properly, someone go poke PartMan");
249+
Logger.errorLog(new Error(`Pet Mods room points: ${JSON.stringify(PSRoomConfigs[roomId])}`));
194250
return;
195251
}
196252

197-
const nonce = randomString();
198-
// TODO: Add mapValues
199-
PSPointsNonce[nonce] = Object.fromEntries(
200-
Object.entries(pointsToAdd).map(([user, amount]) => [user, { [pointsType.id]: amount }])
201-
);
253+
const pointsToAdd: Record<string, number> = {};
254+
[4, 2, 1, 1].forEach((amount, index) => (pointsToAdd[winners[index]] = amount));
202255

203-
room.sendHTML(
204-
<div className="infobox">
205-
<p>
206-
<b>{pointsType.plural}</b>
207-
{': '}
208-
{Object.entries(pointsToAdd)
209-
.map(([user, amount]) => `+${amount} ${user}`)
210-
.join(', ')}
211-
</p>
212-
<p>
213-
<Form value={`/botmsg ${this.status.username},${prefix}@${roomId} addnonce ${nonce}`}>
214-
<button>Add Points!</button>
215-
</Form>
216-
</p>
217-
</div>,
218-
{ rank: '%' }
219-
);
256+
bulkAddPoints(toBulkData(labelPoints(pointsToAdd, pointsType.id)), roomId).then(res => {
257+
if (!res) return;
258+
room.sendHTML(
259+
<div className="infobox">
260+
Added points:{' '}
261+
{Object.entries(pointsToAdd).map(([user, amount]) => (
262+
<>
263+
<Username name={user} />: {pluralize(amount, pointsType)}
264+
</>
265+
))}
266+
</div>,
267+
{
268+
rank: '%',
269+
}
270+
);
271+
showLeaderboard();
272+
});
273+
break;
220274
}
221275
}
222276
}

0 commit comments

Comments
 (0)