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

Add Mafia search capability #10467

Closed
wants to merge 27 commits into from
Closed
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
39d76be
Mafia: Prevent incorrect alignment.
Lucas-Meijer Jan 28, 2023
6509702
Mafia: Update readability.
Lucas-Meijer Jan 28, 2023
a345222
Merge pull request #1 from smogon/master
Lucas-Meijer Nov 7, 2023
f45a045
Merge branch 'master' of https://github.com/Lucas-Meijer/pokemon-show…
May 27, 2024
da8c7a2
Merge branch 'master' of https://github.com/Lucas-Meijer/pokemon-show…
Jun 1, 2024
b352d5e
Implement mafia search, mafia randomdata
Jun 3, 2024
6f2c746
Add Mafia Autohost
Jun 25, 2024
79933c5
setup autohost
Jul 2, 2024
e100d21
Test commit
Jul 7, 2024
2c32677
Scav and Mafia changes
Jul 10, 2024
e0ea6f1
Scavengers fix image scroll
Jul 11, 2024
7e7285d
Merge pull request #2 from Lucas-Meijer/scav-images
Lucas-Meijer Jul 11, 2024
57f4a67
Merge branch 'smogon:master' into master
Lucas-Meijer Jul 11, 2024
c947220
Remove tags
Jul 29, 2024
18ba599
Improve theme searching
Jul 30, 2024
6bbdca0
Improve for npm test
Aug 2, 2024
79253ba
Merge branch 'smogon:master' into mafia-search
Lucas-Meijer Aug 2, 2024
b88b3ce
Remove unwanted changes
Aug 2, 2024
65baee0
Merge branch 'mafia-search' of https://github.com/Lucas-Meijer/pokemo…
Aug 2, 2024
1a773d2
Remove scavengers tab added
Aug 2, 2024
3f68014
Fix TypeScript type errors
Aug 4, 2024
78bdeac
Add StartsWith instead of indexing
Aug 4, 2024
e4426c8
Fix TypeScript errors, idea searching
Aug 13, 2024
c7d8258
Remove more autohost
Aug 13, 2024
0cdc93c
Start fixing types Mafia
Aug 21, 2024
63a0379
Edit Mafia
Aug 30, 2024
a4d45b4
Merge branch 'master' into mafia-search
KrisXV Oct 19, 2024
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
268 changes: 256 additions & 12 deletions server/chat-plugins/mafia.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Utils, FS} from '../../lib';

interface MafiaData {
[key: string]: any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typing says "you can put anything in a MafiaData object at all! We have no idea whats in here as a result" and thats bad. Is there a more specific typing you could be using? Why do we need this typing?

// keys for all of these are IDs
alignments: {[k: string]: MafiaDataAlignment};
roles: {[k: string]: MafiaDataRole};
Expand Down Expand Up @@ -151,12 +152,26 @@ function writeFile(path: string, data: AnyObject) {
MafiaData = readFile(DATA_FILE) || {alignments: {}, roles: {}, themes: {}, IDEAs: {}, terms: {}, aliases: {}};
if (!MafiaData.alignments.town) {
MafiaData.alignments.town = {
name: 'town', plural: 'town', memo: [`This alignment is required for the script to function properly.`],
name: 'Town',
plural: 'Town',
memo: [`This alignment is required for the script to function properly.`],
};
}
if (!MafiaData.alignments.solo) {
MafiaData.alignments.solo = {
name: 'solo', plural: 'solo', memo: [`This alignment is required for the script to function properly.`],
name: 'Solo',
plural: 'Solo',
memo: [`This alignment is required for the script to function properly.`],
};
}
if (!MafiaData.themes.nominations) {
MafiaData.themes.nominations = {
name: `Nominations`,
desc: `Every odd night, the Mafia select three players. The next day, you can only vote for one of these three players.`,
7: `Mafia Goon, Mafia Goon, Villager, Villager, Villager, Villager, Villager`,
11: `Mafia Goon, Mafia Goon, Mafia Goon, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager`,
15: `Mafia Goon, Mafia Goon, Mafia Goon, Mafia Goon, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager`,
19: `Mafia Goon, Mafia Goon, Mafia Goon, Mafia Goon, Mafia Goon, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager, Villager`,
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does nommy need to be hardcoded here, isn't theme data usually just located in the appropriate JSON file?


Expand Down Expand Up @@ -321,6 +336,7 @@ class Mafia extends Rooms.RoomGame<MafiaPlayer> {
dlAt: number;

IDEA: MafiaIDEAModule;

constructor(room: ChatRoom, host: User) {
super(room);

Expand All @@ -330,9 +346,12 @@ class Mafia extends Rooms.RoomGame<MafiaPlayer> {
this.started = false;

this.theme = null;
this.hostid = toID("");
this.host = "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.hostid = toID("");
this.host = "";

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your setting these values below, why set them to empty strings here?


this.hostid = host.id;
this.host = Utils.escapeHTML(host.name);
this.host = Utils.escapeHTML(host.name) && '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.host = Utils.escapeHTML(host.name) && '';
this.host = Utils.escapeHTML(host.name);

&& ''; will mean this always returns '', you probably meant || ''; to say "use the HTML escaped host name or an empty string as a default". However, that isn't needed, if you look at the definition for escapeHTML, you can see that it always returns a string (even an empty one if needed).


this.cohostids = [];
this.cohosts = [];

Expand Down Expand Up @@ -1876,6 +1895,18 @@ class Mafia extends Rooms.RoomGame<MafiaPlayer> {
if (this.IDEA.timer) clearTimeout(this.IDEA.timer);
super.destroy();
}

hostPregame() {

}

hostDay() {

}

hostNight() {

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have three empty methods?

}

export const pages: Chat.PageTable = {
Expand Down Expand Up @@ -2187,7 +2218,6 @@ export const commands: Chat.ChatCommands = {
}
return this.parse('/help mafia');
},

forcehost: 'host',
nexthost: 'host',
host(target, room, user, connection, cmd) {
Expand Down Expand Up @@ -3988,7 +4018,7 @@ export const commands: Chat.ChatCommands = {
let foundTarget = false;
for (const entry of ['alignments', 'roles', 'themes', 'IDEAs', 'terms'] as (keyof MafiaData)[]) {
const dataEntry = MafiaData[entry];
if (from in dataEntry) return this.errorReply(`${from} is already a ${entry.slice(0, -1)}`);
if (from in dataEntry) return this.errorReply(`${from} is already a ${entry.toString().slice(0, -1)}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entry should already implicitly defined as a string, this extra method call isn't needed.

If its not its likely a problem caused by introducing [key: string]: any as a type on MafiaData.

if (to in dataEntry) foundTarget = true;
}
if (!foundTarget) return this.errorReply(`No database entry exists with the key ${to}.`);
Expand Down Expand Up @@ -4044,23 +4074,221 @@ export const commands: Chat.ChatCommands = {
this.sendReply(`The entry ${entry} was deleted from the ${source} database.`);
},
deletedatahelp: [`/mafia deletedata source,entry - Removes an entry from the database. Requires % @ # &`],
listdata(target, room, user) {
if (!(target in MafiaData)) {
return this.errorReply(`Invalid source. Valid sources are ${Object.keys(MafiaData).join(', ')}`);

randtheme: 'listdata',
randrole: 'listdata',
randalignment: 'listdata',
randidea: 'listdata',
randterm: 'listdata',
randroles: 'listdata',
randalignments: 'listdata',
randideas: 'listdata',
randterms: 'listdata',
randdata: 'listdata',
randomtheme: 'listdata',
randomrole: 'listdata',
randomalignment: 'listdata',
randomidea: 'listdata',
randomterm: 'listdata',
randomdata: 'listdata',
randomthemes: 'listdata',
randomroles: 'listdata',
randomalignments: 'listdata',
randomideas: 'listdata',
randomterms: 'listdata',
randthemes: 'listdata',
listthemes: 'listdata',
listroles: 'listdata',
listalignments: 'listdata',
listideas: 'listdata',
listterms: 'listdata',
themes: 'listdata',
roles: 'listdata',
alignments: 'listdata',
ideas: 'listdata',
terms: 'listdata',
ds: 'listdata',
search: 'listdata',
random: 'listdata',
list: 'listdata',
listdata(target, room, user, connection, cmd, message) {
if (!this.runBroadcast()) return false;

// Determine non-search targets first, afterwards searching is done with the remainder
const targets = target.split(',').map(x => x.trim().toLowerCase());

// Determine search type
let searchType = '';
if (cmd.includes('theme') || targets.includes(`themes`)) {
searchType = `themes`;
if (targets.includes(`themes`)) targets.splice(targets.indexOf(`themes`), 1);
} else if (cmd.includes('role') || targets.includes(`roles`)) {
searchType = `roles`;
if (targets.includes(`roles`)) targets.splice(targets.indexOf(`roles`), 1);
} else if (cmd.includes('alignment') || targets.includes(`alignments`)) {
searchType = `alignments`;
if (targets.includes(`alignments`)) targets.splice(targets.indexOf(`alignments`), 1);
} else if (cmd.includes('idea') || targets.includes(`ideas`)) {
searchType = `IDEAs`;
if (targets.includes(`ideas`)) targets.splice(targets.indexOf(`ideas`), 1);
} else if (cmd.includes('term') || targets.includes(`terms`)) {
searchType = `terms`;
if (targets.includes(`terms`)) targets.splice(targets.indexOf(`terms`), 1);
} else if (targets.includes(`aliases`)) {
searchType = `aliases`;
} else if (cmd === 'random' || cmd === 'randomdata' || cmd === 'randdata') {
searchType = [`themes`, `roles`, `alignments`, `IDEAs`, `terms`][Math.floor(Math.random() * 5)];
} else {
return this.errorReply(`Invalid source. Valid sources are ${Object.keys(MafiaData).filter(key => key !== `aliases`).join(', ')}.`);
}

const dataSource = MafiaData[searchType as keyof MafiaData];

const random = (cmd.includes('rand') || targets.includes(`random`));

if (targets.includes(`random`)) targets.splice(targets.indexOf(`random`), 1);

const hidden = (targets.includes(`hidden`));
if (hidden) targets.splice(targets.indexOf(`hidden`), 1);

const shuffle = function (array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[j], array[i]] = [array[i], array[j]];
}
return array;
};

const search = function (entries: any[], searchTarget: string) {
if (typeof (entries) === 'undefined') return entries;

if (searchTarget.length === 0) return entries;
const negation = searchTarget.startsWith('!');

if (negation) searchTarget = searchTarget.substring(1).trim();
const entriesCopy = entries.slice();

const alias = toID((toID(searchTarget) in MafiaData[`aliases`]) ?
MafiaData[`aliases`][toID(searchTarget)] : searchTarget);

if (searchType === `themes` && searchTarget.includes(`players`)) {
const inequalities = ['<=', '>=', '=', '<', '>'];
const inequality = inequalities.find(x => searchTarget.includes(x));
if (!inequality) return entries; // this.errorReply(`Please provide a valid inequality for the players.`);

const players = searchTarget.split(inequality)[1].trim();
if (((players !== null) &&
(players !== '') &&
!isNaN(Number(players)))) {
if (inequality === '=') {
entries = entries.filter(([key, data]) => players in (MafiaData[searchType][key]));
} else if (inequality === '<' || inequality === '<=') {
entries = entries.filter(([key, data]) => ([...Array(+players + (inequality === '<=' ? +1 : +0)).keys()])
.some(playerCount => playerCount in (MafiaData[searchType][key])));
} else if (inequality === '>' || inequality === '>=') {
entries = entries.filter(([key, data]) => ([...Array(50 - Number(players)).keys()]
.map(num => +num + +players + (inequality === '>=' ? +0 : +1)))
.some(playerCount => playerCount in (MafiaData[searchType][key])));
}
} else {
return entries;
}
} else if (searchType === `themes` && alias in MafiaData[`roles`]) {
entries = entries.filter(([key, data]) => ([...Array(50).keys()])
.some(playerCount => playerCount in (MafiaData[searchType][key]) &&
(MafiaData[searchType][key])[playerCount].toString().toLowerCase().includes(alias)));
} else if (searchType === `IDEAs` && alias in MafiaData[`roles`]) {
entries = entries.filter(([key, data]) => MafiaData[`IDEAs`][key].roles.map(role =>
toID((toID(role) in MafiaData[`aliases`]) ? MafiaData[`aliases`][toID(role)] : role)).includes(alias));
} else if (searchType === `roles` && alias in MafiaData[`themes`]) {
entries = entries.filter(([key, data]) => Object.keys(MafiaData[`themes`][alias])
.filter((newKey: any) => toID((MafiaData[`themes`][alias])[newKey].toString()).includes(key)).length > 0);
} else if (searchType === `roles` && alias in MafiaData[`IDEAs`]) {
entries = entries.filter(([key, data]) => MafiaData[`IDEAs`][alias].roles.map(role =>
toID((toID(role) in MafiaData[`aliases`]) ? MafiaData[`aliases`][toID(role)] : role)).includes(key));
} else {
entries = entries.filter(([key, data]) => Object.keys((MafiaData[searchType][key]))
.filter((newKey: any) => (MafiaData[searchType][key])[newKey]
.toString().toLowerCase().includes(searchTarget)).length > 0);
}
return negation ? entriesCopy.filter(element => !entries.includes(element)) : entries;
};

// Number of results
let number = random ? 1 : 0;
for (let i = 0; i < targets.length; i++) {
if (((targets[i] !== null) &&
(targets[i] !== '') &&
!isNaN(Number(targets[i].toString())))) {
number = Number(targets[i]);
targets.splice(i, 1);
break;
}
}
const dataSource = MafiaData[target as keyof MafiaData];

// Convert to rows
const themeRow = function (theme: MafiaDataTheme, players = 0) {
return `<tr><td style="text-align:left;width:30%" ><button class="button" name = "send" value = "/mafia theme ${theme.name}" > ${theme.name} </button> </td><td style="text-align:left;width:70%">${players > 0 ? theme[players] : theme.desc} </td></tr >`;
};
const ideaRow = function (idea: MafiaDataIDEA) {
return `<tr><td style="text-align:left;width:100%" ><button class="button" name = "send" value = "/mafia dt ${idea.name}" > ${idea.name} </button> </td></tr >`;
};
const row = function (role: MafiaDataRole) {
return `<tr><td style="text-align:left;width:30%" ><button class="button" name = "send" value = "/mafia role ${role.name}" > ${role.name} </button> </td><td style="text-align:left;width:70%">${role.memo.join(' ')} </td></tr >`;
};

if (dataSource === MafiaData.aliases) {
room = this.requireRoom();
this.checkCan('mute', null, room);
const aliases = Object.entries(MafiaData.aliases)
.map(([from, to]) => `${from}: ${to}`)
.join('<br/>');
return this.sendReplyBox(`Mafia aliases:<br/>${aliases}`);
} else {
const entries = Object.entries(dataSource)
.map(([key, data]) => `<button class="button" name="send" value="/mafia dt ${key}">${data.name}</button>`)
let table = `<div style="max-height:300px;overflow:auto;"><table border="1" style="border: 1px solid black;width: 100%">`;
let entries = Object.entries(dataSource).sort();

for (const targetString of targets) {
entries = targetString.split('|').map(x => x.trim())
.map(searchTerm => search.call(this, entries.slice(), searchTerm))
.reduce((aggregate, result) => [...new Set([...aggregate, ...result])]);
}

if (typeof (entries) === 'undefined') return;

if (random) entries = shuffle(entries);
if (number > 0) entries = entries.slice(0, number);

if (entries.length === 0) {
return this.errorReply(`No ${searchType} found.`);
}

if (entries.length === 1) {
this.target = entries[0][0];
return this.run((Chat.commands.mafia as Chat.ChatCommands).data as Chat.AnnotatedChatHandler);
}

table += entries
.map(([key, data]) => searchType === `themes` ?
themeRow(MafiaData[searchType][key]) : searchType === `IDEAs` ?
ideaRow(MafiaData[searchType][key]) : row(MafiaData[searchType][key]))
.join('');
return this.sendReplyBox(`Mafia ${target}:<br/>${entries}`);
table += `</table></div>`;
return this.sendReplyBox(table);
}
},
listdatahelp: [
`/mafia roles [parameter, paramater, ...] - Views all Mafia roles. Parameters: theme that must include role, text included in role data.`,
`/mafia themes [parameter, paramater, ...] - Views all Mafia themes. Parameters: roles in theme, players(< | <= | = | => | >)[x] for playercounts, text included in theme data.`,
`/mafia alignments [parameter, paramater, ...] - Views all Mafia alignments. Parameters: text included in alignment data.`,
`/mafia ideas [parameter, paramater, ...] - Views all Mafia IDEAs. Parameters: roles in IDEA, text included in IDEA data.`,
`/mafia terms [parameter, paramater, ...] - Views all Mafia terms. Parameters: text included in term data.`,
`/mafia randomrole [parameter, paramater, ...] - View a random Mafia role. Parameters: number of roles to be randomly generated, theme that must include role, text included in role data.`,
`/mafia randomtheme [parameter, paramater, ...] - View a random Mafia theme. Parameters: number of themes to be randomly generated, roles in theme, players(< | <= | = | => | >)[x] for playercounts, text included in theme data.`,
`/mafia randomalignment [parameter, paramater, ...] - View a random Mafia alignment. Parameters: number of alignments to be randomly generated, text included in alignment data.`,
`/mafia randomidea [parameter, paramater, ...] - View a random Mafia IDEA. Parameters: number of IDEAs to be randomly generated, roles in IDEA, text included in IDEA data.`,
`/mafia randomterm [parameter, paramater, ...] - View a random Mafia term. Parameters: number of terms to be randomly generated, text included in term data.`,
],

disable(target, room, user) {
room = this.requireRoom();
Expand Down Expand Up @@ -4193,6 +4421,22 @@ export const commands: Chat.ChatCommands = {
`/mafia (un)gameban [user], [duration] - Ban a user from playing games for [duration] days. Requires % @ # &`,
].join('<br/>');
buf += `</details>`;
buf += `</details><details><summary class="button">Mafia Dexsearch Commands</summary>`;
buf += [
`<br/><strong>Commands to search Mafia data</strong>:<br/>`,
`/mafia dt [data] - Views Mafia data.`,
`/mafia roles [parameter, paramater, ...] - Views all Mafia roles. Parameters: theme that must include role, text included in role data.`,
`/mafia themes [parameter, paramater, ...] - Views all Mafia themes. Parameters: roles in theme, players(< | <= | = | => | >)[x] for playercounts, text included in theme data.`,
`/mafia alignments [parameter, paramater, ...] - Views all Mafia alignments. Parameters: text included in alignment data.`,
`/mafia ideas [parameter, paramater, ...] - Views all Mafia IDEAs. Parameters: roles in IDEA, text included in IDEA data.`,
`/mafia terms [parameter, paramater, ...] - Views all Mafia terms. Parameters: text included in term data.`,
`/mafia randomrole [parameter, paramater, ...] - View a random Mafia role. Parameters: number of roles to be randomly generated, theme that must include role, text included in role data.`,
`/mafia randomtheme [parameter, paramater, ...] - View a random Mafia theme. Parameters: number of themes to be randomly generated, roles in theme, players(< | <= | = | => | >)[x] for playercounts, text included in theme data.`,
`/mafia randomalignment [parameter, paramater, ...] - View a random Mafia alignment. Parameters: number of alignments to be randomly generated, text included in alignment data.`,
`/mafia randomidea [parameter, paramater, ...] - View a random Mafia IDEA. Parameters: number of IDEAs to be randomly generated, roles in IDEA, text included in IDEA data.`,
`/mafia randomterm [parameter, paramater, ...] - View a random Mafia term. Parameters: number of terms to be randomly generated, text included in term data.`,
].join('<br/>');
buf += `</details>`;

return this.sendReplyBox(buf);
},
Expand Down
Loading