What is the best way to compare two TypeScript object arrays for equality, especially when some objects may have multiple ways to be considered equivalent

Currently, I am in the process of developing a cost function for a game where players are given a set of resources in their hand. The resources can be categorized into different types such as Fire, Air, Water, Earth, Good, Evil, Law, Chaos, and Void.

These resources are further divided into two categories: Elements (Fire, Air, Water, Earth) and Philosophies (Good, Evil, Law, Chaos). In this game, player actions come with a cost that can consist of any combination of resources or categories.

  • For example:
    [Fire, Air, Evil, Philosophy, Philosophy]

To pay a cost, players need to select resources from their hand that correspond to the specified cost. They can use resources from the same category to fulfill category costs, and there is also a wildcard resource called Void that can be used to pay for any other resource.

  • A valid payment for the example cost mentioned above could be [Void, Void, Air, Good, Good]

However, my current evaluation function is struggling to handle substitutions of categories or the Void resource. It currently only checks for exact equivalence:

  /** Returns true if the resources currently selected by the player
   * satisfy the @cost of the option selected */
  evaluatePayment(cost: Resource[]): boolean {
    let isValidPayment: boolean = true;
    if (cost) {
      const playerSelectedResources: Resource[] = this.playerHand
        .filter(r => r.isSelected)
        .map(hr => hr.type);
      // Check that selected resources cover cost
      const missingCost = cost.filter(r => playerSelectedResources.indexOf(r));
      // Check that no additional resources are selected
      const excessPaid = playerSelectedResources.filter(r => cost.indexOf(r));
      if (missingCost.length > 0 || excessPaid.length > 0) {
        isValidPayment = false;
      }
    }
    return isCostSelected;
  }

After receiving feedback, I have restructured the problem as follows:

enum Resource {
  FIRE,
  AIR,
  WATER,
  EARTH,
  GOOD,
  EVIL,
  LAW,
  CHAOS,
  VOID,
  ELEMENT,    
  PHILOSOPHY, 
  ANY         
}

export const ElementResources: Resource[] = [Resource.FIRE, Resource.AIR, Resource.WATER, Resource.EARTH];
export const PhilosophyResources: Resource[] = [Resource.GOOD, Resource.EVIL, Resource.LAW, Resource.CHAOS];


function isValidExactPayment(cost: Resource[], payment: Resource[]): boolean {
  /** Logic here
   * Returns True if payment matches cost exactly, 
   * according to the rules above */
}

Further examples are provided for better understanding:

/** Example 1 */
const cost: Resource[] = [Resource.WATER, Resource, EVIL];

isValidExactPayment(cost, [Resource.WATER, Resource.EVIL]); // true
isValidExactPayment(cost, [Resource.EVIL, Resource.VOID]); // true
isValidExactPayment(cost, [Resource.VOID, Resource.EVIL]); // true, order should not matter
isValidExactPayment(cost, [Resource.WATER, Resource.VOID]); // true

...
(Additional examples were provided but omitted for brevity)

...

I am facing challenges in implementing a more advanced cost evaluation function due to the complexity of the requirements.

Answer №1

To simplify things, you can rearrange the resources and costs. For instance, ANY is considered a cost rather than a resource. Here's one approach:

enum Elements {
  FIRE = 'fire',
  AIR = 'air',
  WATER = 'water',
  EARTH = 'earth',
}

enum Philosophies {
  GOOD = 'good',
  EVIL = 'evil',
  LAW = 'law',
  CHAOS = 'chaos',
}

enum Void {
  VOID = 'void',
}

enum Resource {
  VOID = Void.VOID,
  FIRE = Elements.FIRE,
  AIR = Elements.AIR,
  WATER = Elements.WATER,
  EARTH = Elements.EARTH,
  GOOD = Philosophies.GOOD,
  EVIL = Philosophies.EVIL,
  LAW = Philosophies.LAW,
  CHAOS = Philosophies.CHAOS,
}

const ANY = 'any';
const ELEMENT = 'element';
const PHILOSOPHY = 'philosophy';
type Cost = Resource | typeof ANY | typeof ELEMENT | typeof PHILOSOPHY;

I prefer using string constants over numeric enums as they are easier for debugging.

There are other logical groupings here; for example, a cost of ELEMENT can be paid with either Elements or Void, so we can create data structures for those. Converting the enums into arrays will also aid in iteration.

const allResources = Object.values(Resource);

const elementsAndVoid: (Elements | Void)[] = [];
for (const e of Object.values(Elements)) elementsAndVoid.push(e);
elementsAndVoid.push(Void.VOID);

const philosophiesAndVoid: (Philosophies | Void)[] = [];
for (const p of Object.values(Philosophies)) philosophiesAndVoid.push(p);
philosophiesAndVoid.push(Void.VOID);

Before processing, it's important to determine the quantities of each resource in both the cost and payment arrays. Storing the resources as key-value pairs rather than arrays would make this task much simpler (e.g., { fire: 2, water: 3 }). Although I'll convert the array to key-value pairs within the function, consider refactoring your code accordingly.

If all costs and payments are accounted for, we can cover each cost by decrementing it by one and reducing the corresponding payment resource by one as well. This should be done sequentially based on specificity to avoid shortages. For instance, if we exhaust our FIRE paying for ELEMENT, we may lack sufficient FIRE when required later, even if we had excess WATER to cover ELEMENT. Hence, specific costs take precedence.

If a cost remains greater than 0 without any available payment, we cannot meet that cost.

Below is an implementation:

function isValidExactPayment(costs: Cost[], payments: Resource[]): boolean {
  // count payment amounts
  const paymentCounts: { [key: string]: number } = {};
  for (const p of payments) {
    if (paymentCounts[p] === undefined) paymentCounts[p] = 0;
    paymentCounts[p]++;
  }
  // count cost amounts
  const costCounts: { [key: string]: number } = {};
  for (const c of costs) {
    if (costCounts[c] === undefined) costCounts[c] = 0;
    costCounts[c]++;
  }
  // Attempt to pay for specific resource - void first
  for (const r of allResources) {
    while (costCounts[r] > 0) {
      if (paymentCounts[r] > 0) {
        costCounts[r]--;
        paymentCounts[r]--;
      }
      // Use leftover void if there's not enough
      else if (paymentCounts[Resource.VOID] > 0) {
        costCounts[r]--;
        paymentCounts[Resource.VOID]--;
      }
      // Not enough specific resource
      else {
        console.log('Not enough:', r);
        return false;
      }
    }
  }
  // Attempt to pay for general elements
  for (const r of elementsAndVoid) {
    while (costCounts[ELEMENT] > 0 && paymentCounts[r] > 0) {
      costCounts[ELEMENT]--;
      paymentCounts[r]--;
    }
  }
  // Not enough elements
  if (costCounts[ELEMENT] > 0) {
    console.log('Not enough:', ELEMENT);
    return false;
  }
  // Attempt to pay for general philosophies
  for (const r of philosophiesAndVoid) {
    while (costCounts[PHILOSOPHY] > 0 && paymentCounts[r] > 0) {
      costCounts[PHILOSOPHY]--;
      paymentCounts[r]--;
    }
  }
  // Not enough philosophies
  if (costCounts[PHILOSOPHY] > 0) {
    console.log('Not enough:', PHILOSOPHY);
    return false;
  }
  // Attempt to pay for any with anything
  for (const r of allResources) {
    while (costCounts[ANY] > 0 && paymentCounts[r] > 0) {
      costCounts[ANY]--;
      paymentCounts[r]--;
    }
  }
  // Not enough any
  if (costCounts[ANY] > 0) {
    console.log('Not enough:', ANY);
    return false;
  }
  // Paid in full :)
  console.log('Paid in full');
  return true;
}

While there are obvious performance optimizations that can be made, this solution gets the job done.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The .ts source file is mysteriously missing from the development tool's view after being deployed

When I work locally, I can see the .ts source files, but once I deploy them, they are not visible in any environment. Despite setting my sourcemap to true and configuring browserTargets for serve, it still doesn't work. Can someone help with this issu ...

Monitoring the download status in the web browser: Angular 5

Currently, I'm in the process of developing an application that interacts with a REST API to download files. As per the API's behavior, it responds with the file immediately upon request. So, I've implemented the following logic to facilitat ...

Waiting for the execution of the loop to be completed before proceeding - Typescript (Angular)

There's a code snippet triggered on an HTML page when clicked: public salaryConfirmation() { const matDialogConfig: MatDialogConfig = _.cloneDeep(GajiIdSettings.DIALOG_CONFIG); this.warningNameList = []; for(let i=0; i < this.kelolaDat ...

Converting SQL COUNT query to angularfire2: A guide on translating Firebase / angularfire2

In my firebase database, I have the following structure: "users" : { "USER1_ID" : { "email" : "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="41343224337001393939396f222e2c">[email protected]</a>", ...

Generate a basic collection of strings from an object

Looking at this object structure Names = [ { group: 'BII', categories: null }, { group: 'GVL', categories: [] } ]; I ...

Show Firebase data on Angular 6 mat-card in a randomized sequence when the page loads or refreshes

Is it possible to display cards in a random order when the value of a form changes or when the page is refreshed, while fetching data from Firebase? Below is my Component Template: <ng-container *ngFor="let geoToDisplay of geosToDisplay | async"> ...

The React-widgets DateTimePicker is malfunctioning and displaying an error stating that it cannot assign to the type of 'Readonly<DateTimePickerProps>'

Hello there! I am a newcomer to TypeScript and I am facing difficulty in understanding an error. Any help regarding this will be greatly appreciated. I have tried searching for solutions online and even attempted changing the version, but I am unsure of wh ...

What allows mapped types to yield primitive outputs when using {[P in keyof T]}?

Check out this innovative mapped type example that utilizes the power of keyof: type Identity<T> = { [P in keyof T]: T[P]; }; Have you ever wondered why Identity<number> results in the primitive number type, rather than an object type? Is th ...

Triggering a subsequent action in Ngrx depending on the data from the initial action

Currently, I am fetching a list of users using ngrx: this.users$ = this.store.select(fromReducer.getUsers); In my HTML template: <ul> <li *ngFor="let user of users$ | async"> {{user.id}} - {{user.name}} - {{user.email}} </ ...

Tips for correctly specifying the theme as a prop in the styled() function of Material UI using TypeScript

Currently, I am utilizing Material UI along with its styled function to customize components like so: const MyThemeComponent = styled("div")(({ theme }) => ` color: ${theme.palette.primary.contrastText}; background-color: ${theme.palette.primary.mai ...

npm encountered an issue when attempting to install a package from a local directory: EISDIR: illegal operation on a directory, read

While attempting to add my compiled TypeScript output as a local package using npm, this error appears: $ npm install --save ../app/out npm ERR! eisdir EISDIR: illegal operation on a directory, read npm ERR! eisdir This is most likely not a problem wit ...

Discover the steps to dynamically alter the inclusion of the Bootstrap CSS file within an Angular project

I manage a multi-language website in both English (EN) and Arabic (AR). Currently, I am utilizing Bootstrap CSS from a CDN and adjusting the CDN link based on the selected language. index.html <!DOCTYPE html> <html lang="en"> <h ...

Implementing Global Value Assignment Post Angular Service Subscription

Is there a way to globally assign a value outside of a method within my app component? This is how my service is structured: import { NumberInput } from '@angular/cdk/coercion'; import { HttpClient } from '@angular/common/http'; import ...

Using node-fetch version 3.0.0 with jest results in a SyntaxError stating that import statements cannot be used outside a module

Recently, I've been updating my API to utilize node-fetch 3.0.0. One major change highlighted in their documentation is that node-fetch is now a pure ESM module. Click here for more information on the changes This update caused some of my unit tests ...

Customize the element of the root node of a MUI component using the styled()

I am trying to implement the "component" prop with a MUI component (such as ListItem) using the styled() API. However, I am facing an issue where it says that "component" is not a valid prop. Can someone guide me on how to correctly achieve this? I have se ...

Once the table is created in TypeORM, make sure to insert essential master data such as types and statuses

Hey there, I have a question regarding NestJS and typeORM. My issue is with inserting default values into tables after creating them. For example, I have a priority table where I need to insert High/Medium/Low values. Despite trying everything in the typeo ...

The issue of SVG not displaying on Chrome has been observed in Angular 6

I am working on loading multiple widgets in Angular 6. I have created a loading symbol using SVG, and I am using the following logic to hide and show the loading div until all widgets are loaded. Interestingly, in Firefox, the loading SVG appears as expect ...

Why do selected items in Ionic 3 ion-option not get deselected even after reloading or reinitializing the array

HTML File: <ion-item class="inputpsection" *ngIf="showDeptsec"> <ion-label floating class="fontsize12">Department</ion-label> <ion-select (ionChange)="showDepartmentChosen($event)" multiple="true" formControlName=" ...

Exploring the use of two different array types in the useState hook with TypeScript

Working on a movie gallery project, I am utilizing an API to download movies and TV series. They are then displayed in a Row component where users can click on thumbnails to open them. The challenge arises with TypeScript, as the useState array can receiv ...

How to effectively handle null in Typescript when accessing types with index signatures unsafely

Why am I getting an error that test might be potentially undefined even though I've enabled strictNullCheck in my tsconfig.json file? (I'm unsure of the keys beforehand) const a: Record<string, {value: string}> = {} a["test"].va ...