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

Rework role distribution #6

Open
sudojunior opened this issue Nov 3, 2022 · 9 comments
Open

Rework role distribution #6

sudojunior opened this issue Nov 3, 2022 · 9 comments
Assignees
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@sudojunior
Copy link
Member

Ultimately, I give credit to WolfiaBot for it's implementation in Java for guiding me towards a solution. But I diverge from it's logic by allowing the roles themselves to decide how much they should allocate based on the values provided to them.

  • A new Detective is added every 5 players, after at least 5 players are in game (not including said players).
  • A new Werewolf is added every 4 players.
  • The remaining role slots are handed to Villager

... and the program ends once it logs the role distribution of 1, 2 and 7 respectively.

It is theortically possible to implement this logic into the ratios by having the same failover, and I have no idea how the literal role allocation differs from ratio allocation... likely the same if it's using the same equations. It may also be possible to continue this by allowing multiple greedy roles before it terminates (removing the need for a break, but also offsetting the remaining allocation of roles to the end) - and requiring some sort of distribution code (literal split / round robin / or another ratio).

Role#getChosenRole() is currently set as a placeholder method returning name, but could serve as a role group or wildcard (when extended from Role for both).

Code Source

https://github.com/wolfiabot/Wolfia/blob/5e84f31e/src/main/java/space/npstr/wolfia/game/mafia/MafiaInfo.java#L89
https://github.com/wolfiabot/Wolfia/blob/5e84f31e/src/main/java/space/npstr/wolfia/game/CharakterSetup.java#L30

class Role {
  constructor(name, each, min = 1, afterMin = false) {
    this.name = name;
    this.each = each;
    this.min = min;
    this.afterMin = afterMin;
  }

  // future
  getChosenRole() {
    return this.name;
  }

  quantityFrom(playerCount) {
    // Take any remaining role slots
    if (this.each <= 0) return Infinity;
    if (playerCount < this.min) return 0;
    // ⌊ ({count} - {reset?min:0}) / {each} ⌋
    return Math.floor((playerCount - (this.afterMin ? this.min : 0)) / this.each);
  }
}

const roles = [
  new Role("Detective", 5, 5, true),
  new Role("Villager", 0),
  new Role("Werewolf", 4),
]
  .filter((_, index, array) => array.findIndex((r, i) => r.each === 0 && index === i))
  .sort((a, b) => b.each - a.each);

function getRoleCounts(playerCount) {
  const result = {};
  let remaining = playerCount;

  for (const role of roles) {
    const count = role.quantityFrom(playerCount);

    if (count === 0) continue;

    else if (count === Infinity) {
      result[role.name] = remaining;
      console.log("Found %s, it will take all remaining slots", role.name);
      break;
    }

    else {
      result[role.name] = count;
      remaining -= count;
    }
  }

  return result;
}

console.log(getRoleCounts(10));
@sudojunior sudojunior added documentation Improvements or additions to documentation enhancement New feature or request labels Nov 3, 2022
@sudojunior sudojunior self-assigned this Nov 3, 2022
@sudojunior
Copy link
Member Author

Methods of distribution:

  • Ratio - ⌊ x% ⌋ of y players
  • Ratio after - ^ and after z players` (and optional exclusion*1)
  • Each - add x every y players
  • Each after - ^ and after z players (and optional exclusion*1)

*1 There's also each after exclusion - meaning don't include the initial player count needed to activate it's condition.

@sudojunior
Copy link
Member Author

sudojunior commented Nov 3, 2022

Remainder roles (or greedy roles as I sometimes call them) are intended to take the remaining slots of any game, but to do so requires the total ratio to be less than the certainty of 1 and for slots to not be taken up to begin with.

Role slot allocation can be done in three five steps (if the game builder is to function like this):

  1. fixed roles
  2. each roles + exclusions
  3. ratio roles + exclusions
  4. remaining and ratio roles + exclusions
  5. remaining takes the rest of the slots between them

Roles assigned with remaining as their type or as a modifier (depending on how I do it) would also need a limit or a hard break somewhere to prevent overrunning itself. Hard limit for any game under the pretence of using Select Menu Components is 25 to fit every player on one page. Pagination is... possible, but more trouble than it's probably worth at this time.

@sudojunior
Copy link
Member Author

remaining and ratio doesn't work too well with just remaining when they have to share what's left between them... might just scrap 4. altogether to avert that step. Exclusions work well with each... and that's about it.

@sudojunior
Copy link
Member Author

sudojunior commented Nov 4, 2022

Depending on the active context used to explain how this is meant to work, the description of chance differs in it's purpose. For the code stashed in version control, chance refers to the probability that a player has of getting a particular role - regardless of the mode of distribution. For the code that intends to rebuild how the role roster is assembled, chance refers to the probability that the role itself appears within the resulting selection... for the subsequent method of choice.

At the core of the distribution theory, there are two methods to deal roles to a group of players.

  1. Iterate over roles, and players 'make their bid' to get the role on the podium (for(let i ...) blah blah stuff) - auction-bid.
  2. Iterate over players (or customers), a role is selected for them based on the price they offer (it may not be the best or the worst of what they want, but it is based on the available stock) - reducing-stock / market-stall.

reducing-stock was scrapped over a year ago (9d38619) originally to the thought that it was a cause of a distribution error - later found to be associated to math inaccuracy. Despite everything that has happened, I believe it can return and take the place of blind-auction as well as moving towards @faker-js/faker for random value context (which is currently used for interaction-prototypes.

/** This contains the necessary data to *assemble* the role roster that is to be assigned to players. */
interface RoleData {
  /** The type of role behaviour. */
  type: RoleType;
  /** The name of the role */
  name: string;
  /** Minimum number of players required to include this role. */
  activation: number;
  /** The physical chance the role will be included in the roster, after meeting the activation threshold. */
  activationChance: number;
  /** The absolute quantity of the role, after activiation. */
  quantity: number;
  /** Minimum number of roles to add, after activation. */
  minimum: number;
  /** Maximum number of players allowed to play this role. */
  maximum: number;
  /** Include the initial player count that was required to get the role. */
  include: boolean;
  /** The number of players required to add another role. */
  each: number;
  /** The target role to replace when called upon. */
  replace: string;
  /** The chance of the role being given to a player */
  chance: number;
  /** The type of chance behaviour */
  chanceType: ChanceType;
  /** The thresholds to add a new role. */
  thresholds: number[];
}

@sudojunior
Copy link
Member Author

Role grouping:

  • Single
  • Alignment
  • Team
  • Wildcard

Given that asUnique() introduces some complexities, most groupings would require access to both the provided and working role sets to ensure duplication does not occur. The same cannot be immediately guaranteed for roles with quantity ranges (withMinimum(count) and withMaximum(count)), thresholds (withThresholds(...steps: number[])), or fixed quantities (withFixed(count: number)).

While I have no intention to continue working on the RoleBuilder utility I currently hold on local dev, I will add withRange as a short hand for the min and max methods as well as attempt the role grouping (which would require a change in logic internally). Not sure if I'll allow groups within groups, but I will at least need to consider allowing role metadata (including team, alignment, individual chance, etc.). RoleBuilder was step one, everything else is is two through ten. *1

Hellsing Ultimate Abridged - Episode 1 (Spoiler, Trigger Warning)

@sudojunior
Copy link
Member Author

If withRatio adjusted correctly when numbers fall out of range between 0 and 1.

class RoleBuilder {
  // ...
  withRatio(ratio) {
    // 1:R --> r:1
    if (ratio > 1 || ratio < 0)
      ratio = 1 - 1 / ratio;
    this.data.each = ratio;
  }
  // ...
}

@sudojunior
Copy link
Member Author

For less than 0, abs the value... rather than also sending through this branch, which would return as a certain probability.

ratio = Math.abs(ratio);
if (ratio > 1) // etc.
class RoleBuilder {
  // ...
  withRatio(ratio) {
    // 1:R --> r:1
    if (ratio > 1 || ratio < 0)
      ratio = 1 - 1 / ratio;
    // ...
  }
  // ...
}

@sudojunior
Copy link
Member Author

With references to the different methods to assign roles, assembling the role list itself is likely to remain as it is by iterating over its role entries, rather than over the player count (mainly for focus on efficiency and minimal time complexity).

Swapping the loop target would likely increase the number of iterations needed to determine what the program should do (regardless of player count), and more likely to be inconsistent.

@sudojunior
Copy link
Member Author

class Role {
  selectOne(players: number, currentRoles: string[]) {
    const { name, type } = this.data
    const isUnique = type === RoleType.UNIQUE; // new type?
    
    if (Array.isArray(name)) {
      const filteredRoles = name.filter(r => !currentRoles.includes(r));
      // if (filteredRoles.length <= 1) filteredRoles[0]; // ?? undefined
      return chance.pickone(filteredRoles);
    } else {
      return isUnique && currentRoles.includes(name) ? undefined : name;
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant