Bitmasker
- Andy Mikulski
- Technology , Data
- October 29, 2023
Small helper class to track ‘flagged’ state. Works together with an enum
and provides easy toggled/not toggled checks.
Basic Usage
enum MyColors {
Red,
Green,
Blue,
}
const mask = new Bitmasker<MyColors>();
// `set` lets you update the mask
mask.set(MyFlags.Red, MyFlags.Blue);
mask.has(MyFlags.Red) === true;
mask.has(MyFlags.Green) === false;
mask.has(MyFlags.Blue) === true;
// You can also use the NOT operator (~) to unset or check for the absence of a flag:
mask.set(~MyFlags.Red, MyFlags.Green);
// or use `unset` itself:
mask.unset(MyFlags.Blue);
mask.has(MyFlags.Red) === false;
mask.has(MyFlags.Green) === true;
mask.has(~MyFlags.Blue) === true; // note the NOT (~) operator
// You can also compare a bunch of flags at once, including NOT'd flags. This uses the 'AND' operator.
mask.all(~MyFlags.Red, MyFlags.Green) === true;
mask.all(MyFlags.Red, MyFlags.Green, MyFlags.Blue) === false;
// if you want an 'OR' check against a bunch of flags instead, you can use `some`:
mask.some(MyFlags.Red, MyFlags.Green) === true; // Red is not set, but Green is - returning true!
mask.some(~MyFlags.Red, ~MyFlags.Green) === true; // Red is not set - therefor SOME of the requirements are satisfied!
// You can also reset the mask via `clear`:
mask.clear();
Practical Example (Character Traits)
In this example, we use the bitmasker as a state tracker for a video game character’s “traits.” This allows us to attach/remove multiple traits to a given character, and later test for individual or specific combinations of those traits. This allows us to detect when the character is invulnerable, for instance, and can not take damage. This is more practical than, say, having a boolean on the character denoting invulnerability, because in order to add more possible traits, you just update the enum.
enum CharacterTraits {
Flying,
Invulnerable,
ImmuneToPoison,
CanBreatheUnderWater,
// ..etc..
}
class Character {
public traits: Bitmasker<CharacterTraits> = new Bitmasker();
// ... onDamaged, applyDamage, onPickup, etc ...
}
const mario = new Character();
mario.onDamaged = (type, amount) => {
if (mario.traits.has(CharacterTraits.Invulnerable)){
// can't be hurt at all!
return;
}
if (type == POISON && mario.traits.has(CharacterTraits.ImmuneToPoison)){
// can't be poisoned!
return;
}
mario.applyDamage(type, amount);
};
mario.onPickup = (pickup) => {
switch(pickup.type){
case WINGS: mario.traits.set(CharacterTraits.Flying); break;
case STAR: mario.traits.set(CharacterTraits.Invulnerable); break;
}
}
mario.onPickupEffectExpire = (pickup) => {
switch(pickup.type){
case WINGS: mario.traits.unset(CharacterTraits.Flying); break;
case STAR: mario.traits.unset(CharacterTraits.Invulnerable); break;
}
}
Implementation
export default class Bitmasker<E> {
private flags: number;
constructor() {
this.flags = 0;
}
// Sets a flag corresponding to an enum value
public set(...flags: E[]): void {
for (const flag of flags) {
const f = Number(flag);
if (f >= 0) {
this.flags |= (1 << f);
} else {
this.flags &= ~(1 << (~f));
}
}
}
// Clears a flag corresponding to an enum value
public unset(...flags: E[]): void {
if (flags.length === 0) {
this.flags = 0;
return;
}
for (const flag of flags) {
this.flags &= ~(1 << Number(flag));
}
}
// Toggles a flag corresponding to an enum value
public toggle(...flags: E[]): void {
for (const flag of flags) {
this.flags ^= (1 << Number(flag));
}
}
// Checks if a flag corresponding to an enum value is set
public has(flag: E): boolean {
const f = Number(flag);
if (f >= 0) {
return (this.flags & (1 << f)) !== 0;
} else {
return (this.flags & (1 << (~f))) === 0;
}
}
// Checks if multiple flags corresponding to enum values are set
public all(...flags: E[]): boolean {
for (const flag of flags) {
if (!this.has(flag)) {
return false;
}
}
return true;
}
public some(...flags: E[]): boolean {
let val = false;
for (const flag of flags) {
val ||= this.has(flag);
}
return val;
}
}