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

API: Added external api which returns gainExperience #1955

Merged
merged 15 commits into from
Feb 20, 2025

Conversation

AdamAndreatta
Copy link
Contributor

@AdamAndreatta AdamAndreatta commented Feb 9, 2025

Created a function to provide expGain to the getmemberinfo api

Adds the total experience gained based on cycles presented, then returns all of those values in a list.

Copy link
Contributor

@catloversg catloversg left a comment

Choose a reason for hiding this comment

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

You also need to build and test it. I'm pretty sure it does not work in the current state due to the lack of RAM cost information in src\Netscript\RamCostGenerator.ts.

@@ -79,3 +80,62 @@ export function calculateAscensionPointsGain(exp: number): number {
export function calculateAscensionMult(points: number): number {
return Math.max(Math.pow(points / 2000, 0.5), 1);
}

//FUNCTION RETURNS TOTAL EXP GAINED FOR EACH STAT BASED ON THE PARAMS PROVIDED CYCLES, MEMBER, AND TASK
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't write all-caps comments. It's very hard to read.

//RETURNS A LIST OF THOSE TOTAL VALUES IN THIS ORDER [hackExp, strExp, defExp, dexExp, agiExp, chaExp]
//IF THERE ARE NO TASKS ASSIGNED EMPTY ARRAY IS RETURNED
export function getGainExperience(numCycles: number, member: GangMember): number[] | string {
const task = member.getTask();
Copy link
Contributor

Choose a reason for hiding this comment

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

Most parts of this function duplicate code of gainExperience in src\Gang\GangMember.ts. You need to refactor gainExperience in a way that we can reuse it.

@@ -1,5 +1,7 @@
/** All netscript definitions */

import { GangMember } from "src/Gang/GangMember";
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't import anything to NetscriptDefinitions.d.ts. I get that you do this due to how you design getGainExperience, but that API needs to be designed in a different way. More about this later.

*List is returned in this exact order [hackExp, strExp, defExp, dexExp, agiExp, chaExp]
*if a gang member is passed with no current tasks assigned will return a string that says that their is no task currently assigned
*/
getGainExperience(numCycles: number, member: GangMember): number[] | string
Copy link
Contributor

Choose a reason for hiding this comment

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

There are many problems with this API:

  • It requires GangMember, but GangMember is our internal data structure. The player does not have access to this type. We expose GangMemberInfo for member's info.
  • number[] | string is a very bad type for the result. You are returning a string to tell the player that the input/current condition is invalid. Never do this. You should return null/undefined or throw an error.
  • It may be better to return an object containing relevant information instead of an array.
  • RAM cost for this kind of API should not be 0.

You also need to:

  • Validate user input.
  • Add RAM cost information in src\Netscript\RamCostGenerator.ts.

You can check how we implement other APIs for Gang for an example.

I have a suggestion for you: Don't add a new API, just reuse getMemberInformation. Your API returns data for a member, but we already have an API for it (getMemberInformation). That API already returns "gain" for many things: respectGain, wantedLevelGain, moneyGain. It's totally okay to add expGain. By doing this, you only need to refactor gainExperience and skip many things required for a new API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good that should be more than doable I will make those adjustments and run the tests again

@AdamAndreatta
Copy link
Contributor Author

Finished to adjust of the function used your guy's already existing API to send the data to the user. Made a get function though since refactoring felt it would be more volatile since that function is also how members receive their experience in the function. I ended up returning an object of the exp gained from a cycle for each stat.

@@ -146,46 +147,65 @@ export class GangMember {
cha: (this.cha_mult - 1) / 4 + 1,
};
}

gainExperience(numCycles = 1): void {
gainExperience(numCycles = 1): number | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the return type number | undefined?

getMemberExperience(member: GangMember): object | undefined {
const numCycles = 1;
const task = member.getTask();
if (member === undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

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

member cannot be undefined.

@@ -72,6 +72,62 @@ export function calculateMoneyGain(gang: FormulaGang, member: GangMember, task:
return Math.pow(5 * task.baseMoney * statWeight * territoryMult * respectMult, territoryPenalty);
}

//Calculates memberEXP based on member passed and task passed that the member currently has assigned.
//@returns object containing all exp values that would be gain on task compeletion.
export function calculateMemberExperience(numCycles: number, member: GangMember, task: GangMemberTask): object {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use the generic object. You have to define what you return.

chaEXP: 0,
};

const difficultyMult = Math.pow(task.difficulty, 0.9);
Copy link
Contributor

Choose a reason for hiding this comment

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

You still duplicate code in gainExperience.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, so I refactored the function in a way that we no longer duplicate functions, it now just provides that object based on whether the place in the code provides a necessary member or not. When a character is actually gaining exp I merely made it take null since we already have the player object we need to access. Where in the data retrieval API I instead passed the member object to it to grab the exp data and return it. Hopefully that is a much better alternative instead.

@@ -1,6 +1,6 @@
import type { Gang as IGang, EquipmentStats, GangOtherInfoObject } from "@nsdefs";
import type { Gang } from "../Gang/Gang";
import type { GangMember } from "../Gang/GangMember";
import { GangMember } from "../Gang/GangMember";
Copy link
Contributor

@catloversg catloversg Feb 10, 2025

Choose a reason for hiding this comment

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

Is this change necessary? I don't see anything that requires this change.

Copy link
Contributor

Choose a reason for hiding this comment

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

You really need to check this change.

@@ -1004,6 +1003,8 @@ interface GangMemberInfo {
wantedLevelGain: number;
/** Per Cycle Income for this gang member */
moneyGain: number;
/**PerCycle Exp object for each stat for this gang member */
expGain: object | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use the generic object. You have to define what you return. undefined vs null is opinion-based, so it may be fine, but I prefer null in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Complete the fixes you guys mentioned, hopefully this version is much more appealing. My apologies again for the previous iterations.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Jumping in here to say: Iterating is how you learn! (Also, it is common in general, and for catlover and I specifically, to be rather terse in code review suggestions. Don't take that as us being upset; if we were upset, you'd know XD)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries at all, I totally understand! I appreciate you guys taking the time to look over as well as give advice on improving.

…ns, Instead refactoredexperience gain function to pass back an object under specified parameters.
@@ -147,63 +147,99 @@ export class GangMember {
cha: (this.cha_mult - 1) / 4 + 1,
};
}
gainExperience(numCycles = 1): number | undefined {
gainExperience(numCycles = 1, member: GangMember | null): GangMemberExpGain | null {
Copy link
Contributor

@catloversg catloversg Feb 11, 2025

Choose a reason for hiding this comment

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

You need to design and implement this function in a different way.

  • It does not make sense for gainExperience, which is a method of GangMember, receives a member instance.
  • gainExperience, as its name implies, has only one job: increase the exp. You are trying to make it do 2 different jobs at the same time.
  • Again, you still duplicate code here. The moment you copy/paste code, you know that it's wrong.

You can use 2 functions.

  • Function A: calculate the gain.
  • Function B: call A, then increase the calculated gain.

/**PerCycle Exp object for each stat for this gang member */
expGain: object | undefined;
/** Per Cycle Exp object for each stat for this gang member */
expGain: GangMemberExpGain | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

You have to explain the case in which it's null in the documentation.

Copy link
Contributor Author

@AdamAndreatta AdamAndreatta Feb 12, 2025

Choose a reason for hiding this comment

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

Okay, just to make sure that this will work out fine. I separated the functions. One function independently handles the calculation of the exp that should be gained by the member. These calculations were already made in the original gainExperience function, I merely moved to another function within the formulas. I created a second function which receives those calculations in the form of an object from functionA the values from this object are added to the member's values own values. However, I was struggling very much so to find a way to ensure that the API could receive this object without duplicating exp gain. I settled on creating a getter function which is a GangMember function which receives a NumCycles value that is 1 since we are trying to calculate the exp gain of a task per cycle. There is no duplicate code and the calculations only happen once now with each function having one independent job. Please let me know if this works out if not I can attempt to adjust it more.

@d0sboots
Copy link
Collaborator

Thank you catlover for jumping on this while I was being lazy. I'm going to be mostly hands-off on this one (aside from pushing the "run tests" button) until it's close to ready to go.

@AdamAndreatta
Copy link
Contributor Author

AdamAndreatta commented Feb 12, 2025

Ran the formatter, apologies for forgetting that. Everything should be tested and good. Just let me know if I need to make further adjustments.

@@ -72,6 +73,60 @@ export function calculateMoneyGain(gang: FormulaGang, member: GangMember, task:
return Math.pow(5 * task.baseMoney * statWeight * territoryMult * respectMult, territoryPenalty);
}

export function calculateExpGain(numCycles = 1, member: GangMember, task: GangMemberTask): GangMemberExpGain {
Copy link
Contributor

Choose a reason for hiding this comment

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

Task is a property of GangMember. There is no need to pass it to this function.

@@ -1,6 +1,6 @@
import type { Gang as IGang, EquipmentStats, GangOtherInfoObject } from "@nsdefs";
import type { Gang } from "../Gang/Gang";
import type { GangMember } from "../Gang/GangMember";
import { GangMember } from "../Gang/GangMember";
Copy link
Contributor

Choose a reason for hiding this comment

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

You really need to check this change.

// if nothing is provided to the member it returns null
getGangMemberExpGain(numCycles = 1): GangMemberExpGain | null {
const task = this.getTask();
if (task == GangMemberTasks.Unassigned) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Be careful with == and ===. In most cases, using === is the safest choice.

@@ -147,43 +149,28 @@ export class GangMember {
};
}

// Grabs the GangMemberExpGain object based on the passed paramters to be exported to the getMemberInfo API
// if nothing is provided to the member it returns null
getGangMemberExpGain(numCycles = 1): GangMemberExpGain | null {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is unnecessary. The check of GangMemberTasks.Unassigned is duplicated here and in gainExperience. It should be in calculateExpGain. If the task is Unassigned, calculateExpGain returns null. In gainExperience, if calculateExpGain returns null, gainExperience returns immediately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay got rid of the unnecessary function made the calculation a member function which made the most sense, there was no reason to pass all the parameters when it's merely going to be called from member anyway where all the necessary data is present to return the per-cycle exp.

… that it no longer needs to pass parameters it just takes from whatever member is called from and returns their per-cycle exp
return expValues;
}

gainExperience(): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function needs to receive numCycles, then pass it to calculateExpGain. You have to build and test your change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made the change, ran the linter, formatter, and testing suite. Testing suite showed all tests were passed.

@catloversg
Copy link
Contributor

@d0sboots The basic design looks good. Please review the rest. Some things are opinion-based:

  • GangMemberExpGain | null: I prefer returning null when the action is invalid (the current task is Unassigned), so I suggested it. If you think returning an object with exp = 0 is better, please feel free to request that change.
  • Wording of some comments needs to be improved. It's best to let you recommend it.

@AdamAndreatta Our test suite is not really good. For this kind of change, you should (at least) write in-game scripts to test it, copy/paste those scripts and the result here. It's even better if you add more Jest tests, but it may be too hard for a new contributor, so you can skip it.

@AdamAndreatta
Copy link
Contributor Author

AdamAndreatta commented Feb 13, 2025

Presented no input to the function which provided the expected error
image

export async function main(ns){
    let names = ns.gang.getMemberNames();
    let value = ns.gang.getMemberInformation("");
    ns.tprint(value);
}
export async function main(ns){
    let names = ns.gang.getMemberNames();
    let value = ns.gang.getMemberInformation("guy2");
    ns.tprint(value);
}

Provided input when not task was currently assigned, expected result was null and null was presented.
image

Provided input while also ensuring the member was assigned a task, the expected object was presented with the per cycle values.
image

Copy link
Collaborator

@d0sboots d0sboots left a comment

Choose a reason for hiding this comment

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

Looking good, it looks like catlover already put you through the wringer XD

I'm primarily concerned with stuff that affects the public (ns) interface, but these should be quick to fix.

Comment on lines 1021 to 1022
/** Per Cycle Exp object for each stat for this gang member returned as a object containing each stats value
* returns null in the even that the member does not have a given task.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/** Per Cycle Exp object for each stat for this gang member returned as a object containing each stats value
* returns null in the even that the member does not have a given task.
/**
* Per Cycle Exp object for each stat for this gang member.
*
* null in the event that the member does not have a given task.
  • The second part of the sentence was redundant and confusing IMO
  • In docs like this, you need a extra newline, otherwise it will all run together on one line when formatted. (Or, maybe that's what you want, but you will still want a period at the end of the prior sentence.)

return;
}

this.hack_exp += gains.hackEXP;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here and everywhere: Name the fields consistently (should be hack_exp etc.)

Also in the external interface. We're not super consistent, but for stats underscores and lowercase is pretty consistently the style.

Copy link
Contributor Author

@AdamAndreatta AdamAndreatta Feb 18, 2025

Choose a reason for hiding this comment

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

@d0sboots made those fixes for ya if there's anything else ya need me to do lmk, more than happy to do so. Thanks very much to you both for being so helpful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This might seem nitpicky, but it's hack_exp not hack_EXP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@d0sboots fixed it 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't forget to rerun npm doc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@d0sboots sorry about that! completely slipped my mind its updated now

@d0sboots d0sboots merged commit a25ab3d into bitburner-official:dev Feb 20, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants