Skip to content

Rules

Rules are what controls whether a card is able to be passed, or able to be recieved. Every pileElement requires rules, but they default to always allow both passing and receiving.

Rules Properties

PropertyTypeDescriptionDefaultAlternative Options
passRulesArray of Functions that return booleansRules to allow card(s?) to be passed[() => true]Provide Array
receiveRulesArray of Functions that return booleansRules to allow card(s?) to be received[() => true]Provide Array
typeobject {pass: 'every' or 'any', receieve: 'every' or 'any'}Whether all rules, or any rule needs to be true to pass/receieve card{pass:'every', receieve:'every}{pass: 'any', receive: 'any' }
canPassFunction that returns booleanRuns every pass rule, if all are true returns truefunctionnone: required
canReceiveFunction that returns booleanRuns every receive rule, if all are true returns truefunctionnone: required

Rules Type

typescript
export interface RuleSet<T extends Card> {
  canPass: Rule<T>;
  canReceive: Rule<T>;
  type: { pass: "every" | "any"; receive: "every" | "any" };
}

export type Rule<T extends Card> = (
  source: PileElementType<T>,
  destination: PileElementType<T>,
  card: CardElementType<T>,
  ...extraArgs: unknown[]
) => boolean;

Rules Constructor

typescript
  constructor(
    passRules: Rule<T>[] = [() => true],
    receiveRules: Rule<T>[] = [() => true],
    type: { pass: "every" | "any"; receive: "every" | "any" } = {
      pass: "every",
      receive: "every",
    },
  ) {
    this.passRules = passRules;
    this.receiveRules = receiveRules;
    this.type = type;
  }

Creating New Rules

Every game will require different rules, as well as every pile will have differing rules. Lets make a rule for our crazy 8's game.

typescript
const onlyPassToDiscard: Rule<PlayingCard> = (
  source,
  destination,
  cardElement,
) => {
  if (destination.pile.name === "discard") return true;
  else return false;
};

const rules = new Rules([onlyPassToDiscard], [() => true]);
hand.options.rules = rules;

Please note: this will be up to us to create our discard pile using the name we are looking for. const discard = deck.createPileElement('discard')

Our game will definitely require more rules than just playing cards to discard, but at least now we can't pass cards to draw pile or to another player!

Lets look into following suit.

typescript
const followSuit: Rule<PlayingCard> = (source, destination, cardElement) => {
  const card = cardElement.card;
  const destTopCard = destination.topCardElement.card;
  if (destTopCard.suit === card.suit) return true;
  else return false;
};

const rules = new Rules([onlyPassToDiscard, followSuit], [() => true]);
hand.options.rules = rules;

Now every time our player wants to pass a card it must be to the discard pile, and it must follow suit!

Oh No! But what if we want to play an 8? Now we can only play it if the 8 is the same suit...

Let's make some changes.

typescript
const followSuitOrPlayAnEight: Rule<PlayingCard> = (
  source,
  destination,
  cardElement,
) => {
  const card = cardElement.card;
  if (card.number === "8") return true;
  const destTopCard = destination.topCardElement.card;
  if (destTopCard.suit === card.suit) return true;
  else return false;
};

const rules = new Rules(
  [onlyPassToDiscard, followSuitOrPlayAnEight],
  [() => true],
);
hand.options.rules = rules;

Why did we adjust the follow suit rule and not just make a new rule that would be true if the card was an 8? EVERY rule needs to be true in order to pass a card by default. This means we may have to shortcut some rules with base cases. If you find yourself having multiple base cases (likely) a shortcut function may be helpful, or changing the rules to allow ANY true to pass may suffice.

typescript
const allowAnEight = () => {}; // ...code
const allowSameNumber = () => {}; // ...code
const allowSomethingElse = () => {}; // ...code

const baseCases: Rule<PlayingCard> = (source, destination, cardElement) => {
  if (allowAnEight(source, destination, cardElement) === true) return true;
  if (allowSameNumber(source, destination, cardElement) === true) return true;
  if (allowSomethingElse(source, destination, cardElement) === true)
    return true;
  return false;
};

const followSuit: Rule<PlayingCard> = (source, destination, cardElement) => {
  if (baseCases(source, destination, cardElement) === true) return true;
  const card = cardElement.card;
  const destTopCard = destination.topCardElement.card;
  if (destTopCard.suit === card.suit) return true;
  else return false;
};

const rules = new Rules([onlyPassToDiscard, followSuit], [() => true]);
hand.options.rules = rules;

Now if the card played is an 8, the same number as the last card, or another variable it well default to true.

Alternative - change rules to pass for any true rule

We can change the default type of rules to allow the pass to occur when any of the rules are true. Any and Every will both have flaws, as they both may require base cases.

Lets change the above rules to accomodate ANY rules.

typescript
const allowAnEight = () => {
  if (onlyPassToDiscard(...args) === false) return false;
}; // ... rest of code
const allowSameNumber = () => {
  if (onlyPassToDiscard(...args) === false) return false;
}; // ... rest of code
const allowSomethingElse = () => {
  if (onlyPassToDiscard(...args) === false) return false;
}; // ... rest of code

const followSuit: Rule<PlayingCard> = (source, destination, cardElement) => {
  {
    if (onlyPassToDiscard(source, destination, cardElement) === false)
      return false;
  }
  const card = cardElement.card;
  const destTopCard = destination.topCardElement.card;
  if (destTopCard.suit === card.suit) return true;
  else return false;
};

const rules = new Rules(
  [allowAnEight, allowSameNumber, allowSomethingElse, followSuit],
  [() => true],
);
hand.options.rules = rules;

As you can see, we can accomplish the same outcome, but now onlyPassToDiscard has become a base case in every rule that will return false if the destination isnt the discard pile.

This is up to you to decide which rule variation suits your game, and coding style best. Let's face it, rules are hard, and having a system to accomodate any rule is hard to imagine.

How do I know if it should be a passRule or a receiveRule?

There may not be a clear cut case for every pile. The above rules could be either a players pass rules, or the discard piles receive rules. There is a lot of overlap in a simple game such as crazy 8's, where players can only play in one spot.

Changing Rules

Piles can change rules at any point, just assign new rules under the piles.options.rules property. Common use of changing rules could be dealing cards. For more info on animations like deal, see animations

typescript
hand.options.rules = new Rules([() => true], [() => true]); // allow receiving cards
await deal(7, deck, hand, 100); // deal the cards to hand
hand.options.rules = new Rules([() => true], [() => false]); // now restrict receiving cards

Pre-Made Rules

This gets shipped with a couple of quick and easy rules to implement. Most are directed towards a Solitaire style game.

Quick Pass Rules

Use import statement: import { quickPassRules } from "card-factory"

NameUsageDescription
alwaysPassquickPassRules.alwaysPassAlways allow passing
neverPassquickPassRules.neverPassNever allow passing
onlyFaceUpquickPassRules.onlyFaceUpOnly pass face up cards
onlyTopCardquickPassRules.onlyTopCardOnly pass top card
redBlackAlternatingquickPassRules.redBlackAlternatingAlways a pile if they alternate color, and increase by 1
typescript
const solitarePassRules = [
  quickPassRules.onlyFaceUp,
  quickPassRules.redBlackAlternating,
];
const solitareReceiveRules = [];
const solitareRules = new Rules(solitarePassRules, solitareReceiveRules);

Quick Receive Rules

Use import statement: import { quickReceiveRules } from "card-factory"

NameUsageDescription
alwaysReceivequickPassRules.alwaysReceiveAlways allow receiving
neverReceivequickPassRules.neverReceiveNever allow receiving
emptyAndRedBlackAlternatingquickPassRules.emptyAndRedBlackAlternatingAn Empty Pile Accepts cards always, or if cards alternate red and black
emptyAndOneLessThanTopCardquickPassRules.emptyAndOneLessThanTopCardAn Empty Pile Accepts cards always, or if card is one less than current top card
emptyAndOneMoreThanTopCardquickPassRules.emptyAndOneMoreThanTopCardAn Empty Pile Accepts cards always, or if card is one more than current top card
onlySpecificCardValuequickPassRules.onlySpecificCardValueOnly a certain card will go here
sameSuitPlusOneOrAcequickPassRules.sameSuitPlusOneOrAceIf card is same suit and one more than top card, or an Ace
typescript
const solitareAcePilePassRules = [quickPassRules.neverPass];
const solitareAcePileReceiveRules = [quickPassRules.sameSuitPlusOneOrAce];
const acePileRules = new Rules(
  solitareAcePilePassRules,
  solitareAcePileReceiveRules,
);