Skip to content

Commit

Permalink
Merge pull request #216 from hymccord/hunt-rejection-feature
Browse files Browse the repository at this point in the history
Add hunt rejection engine
  • Loading branch information
AardWolf authored Jan 21, 2023
2 parents fdfc820 + 0f58893 commit 573172c
Show file tree
Hide file tree
Showing 13 changed files with 610 additions and 64 deletions.
21 changes: 0 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions src/scripts/hunt-filter/engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { HgResponse, User } from "../types/hg";
import { IntakeMessage } from "../types/mhct";
import { LoggerService } from "../util/logger";
import { IRule, IPropertyRule, IMessageExemption } from "./interfaces";
import { ResponseRules } from "./responseRules";
import { UserRules } from "./userRules";
import { MessageRules } from "./messageRules";
import { MessageExemptions } from "./messageExemptions";

/**
* Uses pluggable rule to validate data before a hunt can be
* submitted to the database
*/
export class IntakeRejectionEngine {
private logger: LoggerService;
private responseRules: IRule<HgResponse>[] = [];
private userRules: IRule<User>[] = [];
private messageRules: IPropertyRule<IntakeMessage>[] = [];
private messageExemptions: IMessageExemption[] = [];

constructor(logger: LoggerService) {
this.logger = logger;
this.initResponseRules();
this.initUserRules();
this.initMessageRules();
};

public validateResponse(pre: HgResponse, post: HgResponse): boolean {
return this.responseRules.every(r => {
const isValid = r.isValid(pre, post);
if (!isValid) {
this.logger.debug(`Api responses invalid: ${r.description}`);
}
return isValid;
});
}

public validateUser(pre: User, post: User): boolean {
return this.userRules.every(r => {
let isValid = r.isValid(pre, post);
if (!isValid) {
this.logger.debug(`User objects invalid: ${r.description}`);
}
return isValid;
});
}

public validateMessage(pre: IntakeMessage, post: IntakeMessage): boolean {
// TODO: Refactor? Location is being set to null in stage funcs so it'll fail later
if (pre.location === null || post.location === null) {
return false;
}

// Run rules. Build set of currently invalid properties
// { "location", "stage" }
const invalidProperties = new Set<(keyof IntakeMessage)>();
for (const rule of this.messageRules) {
const valid: boolean = rule.isValid(pre, post);
if (!valid) {
this.logger.debug(`Message invalid: ${rule.description}`);
invalidProperties.add(rule.property);
}
}

// Don't have to run exemption rules if there are no violations
if (invalidProperties.size === 0) {
return true;
}

// Find exception objects that will run for given key
const exemption_providers = this.messageExemptions.filter(e => invalidProperties.has(e.property));
this.logger.debug(`Got ${exemption_providers.length} exemption providers for these invalid properties:`, ...invalidProperties.values());

// Do we need to filter them furthur?
// Exception object could give specific key and value to filter on.
// Didn't pre-optimize but could be worth if there are many, many exemptions and they are slow (doubtful)
for (const provider of exemption_providers) {
// Each exemption can give multiple keys that it accounts for.
// For example, the location and stage will change when catching realm ripper
// so that exemption provides [ "location", "stage" ]
const exemptions = provider.getExemptions(pre, post);
if (exemptions && exemptions.length > 0) {
this.logger.debug(`Got exemptions. Description: ${provider.description}`, { properties: exemptions });
exemptions.forEach(e => invalidProperties.delete(e));
}

if (invalidProperties.size == 0) {
this.logger.debug('Message was revalidated due to exemptions.');
return true;
}
}

this.logger.debug(`Message object invalid`, {
properties: { ...invalidProperties.values() },
messages: { pre, post }
})

return false;
}

private initResponseRules() {
this.responseRules = ResponseRules;
}

private initUserRules() {
this.userRules = UserRules;
}

private initMessageRules() {
this.messageRules = MessageRules;
this.messageExemptions = MessageExemptions
}
}
47 changes: 47 additions & 0 deletions src/scripts/hunt-filter/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { IntakeMessage } from "../types/mhct";

/**
* Describes a way to validate a pre and post object
*/
export interface IRule<K> {
/**
* Short statement explaining rule.
*/
readonly description: string;
/**
* Check if two objects are valid between each other.
* @param pre The pre-hunt object
* @param post The post-hunt object
* @returns true if valid, otherwise false
*/
isValid(pre: K, post: K): boolean;
}

/**
* Provides a rule for a specific property of T
*/
export interface IPropertyRule<T> extends IRule<T> {
readonly property: (keyof T);
}

/**
* Contract for implementing exemptions of IntakeMessage
*/
export interface IMessageExemption {
/**
* Short statement explaining exemption.
*/
readonly description: string;
/**
* When this property name is invalid on an intake message,
* this rule will try to provide exemptions.
*/
readonly property: (keyof IntakeMessage);
/**
* Get exemptions for the given pre and post messages
* @param pre The pre-hunt IntakeMessage
* @param post The post-hunt IntakeMessage
* @returns An array of keys (of IntakeMessage) for which properties has been exempted or null.
*/
getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null;
}
27 changes: 27 additions & 0 deletions src/scripts/hunt-filter/messageExemptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { IntakeMessage } from "../types/mhct";
import { IMessageExemption } from "./interfaces";

// Acolyte Realm

/**
* Provides an exemption on the 'location' difference. Iff the mouse was
* a Realm Ripper and the user moved from FG -> AR. Give exemptions for
* 'location' and 'stage'.
*/
class RealmRipperLocationExemption implements IMessageExemption {
readonly description = "Realm Ripper caught in Forbidden Grove";
readonly property = "location";
getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null {
if (pre.location?.name === "Forbidden Grove"
&& post.location?.name === "Acolyte Realm"
&& pre.mouse === "Realm Ripper") {
return [ "location", "stage" ]
}

return null;
}
}

export const MessageExemptions: IMessageExemption[] = [
new RealmRipperLocationExemption,
]
57 changes: 57 additions & 0 deletions src/scripts/hunt-filter/messageRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IntakeMessage } from "../types/mhct";
import { IPropertyRule } from "./interfaces";

class IntakeMessageSameCheese implements IPropertyRule<IntakeMessage> {
readonly description = "Cheese should not change";
readonly property = "cheese";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
return pre.cheese.name === post.cheese.name;
}
}

class IntakeMessageSameWeapon implements IPropertyRule<IntakeMessage> {
readonly description = "Trap should not change";
readonly property = "trap";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
return pre.trap.name === post.trap.name;
}
}

class IntakeMessageSameBase implements IPropertyRule<IntakeMessage> {
readonly description = "Base should not change";
readonly property = "base";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
return pre.base.name === post.base.name;
}
}

class IntakeMessageSameLocation implements IPropertyRule<IntakeMessage> {
readonly description = "Location should not change";
readonly property = "location";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
return (pre.location !== null && post.location !== null)
&& pre.location.name === post.location.name;
}
}

class IntakeMessageSameStage implements IPropertyRule<IntakeMessage> {
readonly description = "Stage should not change";
readonly property = "stage";
isValid(pre: IntakeMessage, post: IntakeMessage): boolean {
// Juggling check. Valid if both stages are undefined or null.
if (pre.stage == null && post.stage == null) {
return true;
}

// stage can be a object so just use stringify
return JSON.stringify(pre.stage) === JSON.stringify(post.stage);
}
}

export const MessageRules: IPropertyRule<IntakeMessage>[] = [
new IntakeMessageSameCheese,
new IntakeMessageSameWeapon,
new IntakeMessageSameBase,
new IntakeMessageSameLocation,
new IntakeMessageSameStage,
]
29 changes: 29 additions & 0 deletions src/scripts/hunt-filter/responseRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { HgResponse } from "../types/hg";
import { IRule } from "./interfaces";

export class HgResponseBothRequireSuccess implements IRule<HgResponse> {
readonly description = "Both responses should have a 'success' of 1";
isValid(pre: HgResponse, post: HgResponse): boolean {
return pre.success === 1 && post.success === 1;
}
}

export class HgResponsePreNeedsPage implements IRule<HgResponse> {
readonly description = "Pre-response should have a 'page' field";
isValid(pre: HgResponse, post: HgResponse): boolean {
return pre.page !== undefined && pre.page !== null;
}
}

export class HgResponseActiveTurn implements IRule<HgResponse> {
readonly description = "Post-response should have true 'active_turn'";
isValid(pre: HgResponse, post: HgResponse): boolean {
return post.active_turn === true;
}
}

export const ResponseRules: IRule<HgResponse>[] = [
new HgResponseBothRequireSuccess,
new HgResponsePreNeedsPage,
new HgResponseActiveTurn,
]
26 changes: 26 additions & 0 deletions src/scripts/hunt-filter/userRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { User } from "../types/hg";
import { IRule } from "./interfaces";

class UserRequiredDifferences implements IRule<User> {
readonly description = "Pre and post user's 'num_active_turns' and 'next_activeturn_seconds' should differ";
readonly required_differences: (keyof User)[] = [
"num_active_turns",
"next_activeturn_seconds"
]

isValid(pre: User, post: User): boolean {
return this.required_differences.every(key => pre[key] != post[key]);
}
}

class UserNumActiveTurnsIncrementedByOne implements IRule<User> {
readonly description = "User number of active turns should increase by 1";
isValid(pre: User, post: User): boolean {
return post.num_active_turns - pre.num_active_turns === 1;
}
}

export const UserRules: IRule<User>[] = [
new UserRequiredDifferences,
new UserNumActiveTurnsIncrementedByOne
]
Loading

0 comments on commit 573172c

Please sign in to comment.