Skip to content

Instantly share code, notes, and snippets.

@Zemnmez
Created June 18, 2020 21:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zemnmez/d37c09e6b0f63f55445367675ddde95f to your computer and use it in GitHub Desktop.
Save Zemnmez/d37c09e6b0f63f55445367675ddde95f to your computer and use it in GitHub Desktop.
calculates what rites are available in the game cultist simulator
/* Rite Calculator */
/* Scroll down to add followers to the follower list!! */
type Principle = Aspect.Forge | Aspect.Heart | Aspect.Winter | Aspect.Edge
| Aspect.Lantern | Aspect.Moth | Aspect.Grail | Aspect.Knock | Aspect.SecretHistories;
enum Kind {
Card, Slot, Recipe
}
enum Aspect {
// Principles
Forge,
Heart,
Winter,
Edge,
Knock,
Moth,
Lantern,
Grail,
Rite,
Influence,
Tool,
Follower,
Lore,
Mortal,
Desire,
Memory,
Prisoner,
SecretHistories,
Ingredient,
RequiresKnowledge,
RequiresIntuition,
RequiresPracticalExperimentation,
Potential,
Disciple,
Remnant,
IllHealth,
Spirit,
DeepMandicSpeaker,
PhrygianSpeaker,
FucineSpeaker
}
const Card = {
kind: Kind.Card
} as const;
const followers: Follower[] = [
{
...Card,
name: "Neville, a believer",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Follower]: 1,
[Aspect.Mortal]: 1
}
},
{
...Card,
name: "Enid, a disciple",
aspects: {
[Aspect.Knock]: 3,
[Aspect.Follower]: 1,
[Aspect.Potential]: 1,
[Aspect.Disciple]: 1,
}
},
]
interface Named {
readonly name?: string
}
interface Described {
readonly description?: string
}
interface Countable {
readonly count?: number
}
interface Aspected {
readonly aspects: {
[aspect in Aspect]?: number
}
}
interface Card extends Named, Described, Aspected, Countable {
readonly kind: Kind.Card
}
interface Follower extends Card {
readonly aspects: Card["aspects"] & {
[Aspect.Follower]: 1
}
}
interface Slot extends Named, Described, Aspected {
readonly kind: Kind.Slot,
/**
* These aspects are 'or'ed, rather than 'and'ed
*/
readonly aspects: Aspected["aspects"],
readonly consumed?: true
}
const Slot = {
kind: Kind.Slot
} as const;
interface Rite extends Card {
readonly slots: readonly Slot[]
readonly aspects: Card["aspects"] & {
[Aspect.Rite]: number
}
}
const Rite = {
...Card,
aspects: {
[Aspect.Rite]: 1
}
} as const;
const Invocation: Slot = {
...Slot,
name: "Invocation",
description: "What are the words?",
aspects: {
[Aspect.Lore]: 1
}
} as const;
const Assistant: Slot = {
...Slot,
name: "Who will speak the words?",
aspects: {
[Aspect.Follower]: 1,
[Aspect.Prisoner]: 1
}
}
const Offering: Slot = {
...Slot,
name: "What influence will be used?",
aspects: {
[Aspect.Influence]: 1
}
}
const OfferingConsumed: Slot = {
...Offering,
name: "What influence will be consumed?",
consumed: true
}
const SunsetRite: Rite = {
...Rite,
name: "Sunset Rite",
aspects: {
[Aspect.Rite]: 1
},
slots: [ Invocation, Assistant, Offering ]
} as const;
const Rites: readonly Rite[] = [SunsetRite] as const;
const LoreNames: Record<Principle,
string[] & {
[n in 0|1|2|3|4|5|6]: string
}
> = {
[Aspect.Lantern]: [
"A Watchman's Secret",
"A Mansus-Glimpse",
"An Unmerciful Mantra",
"Phanaean Invocation",
"Formulae Concursate",
"Mantra of Ascent",
"Illuminate Mysteries"
],
[Aspect.Forge]: [
"A Smith's Secret",
"An Ardent Orison",
"A Shaping Chant",
"Callidate Invocation",
"Formula Fissive",
"Fornace Paean",
"Mysteries of Making"
],
[Aspect.Edge]: [
"A Knife's Secret",
"Chiliarch's Lesson",
"An Operation of the Labhite",
"The Colonel's Names",
"The Lionsmith's Names",
"The Alignments of Murder",
"The Mysteries of Force",
],
[Aspect.Winter]: [
"A Sexton's Secret",
"A White Ceremony",
"An Operation of the Declining Sun",
"Invocation of the Ivory Dove",
"Recitation of the Lost Hours",
"The Divisions of the Names",
"Wolf-Word",
],
[Aspect.Heart]: [
"A Thunderous Secret",
"Words that Walk",
"A Waking Chant",
"Geminate Invocation",
"Formulae Vigilant",
"Velvet Charm",
"Unceasing Mysteries"
],
[Aspect.Grail]: [
"A Red Secret",
"A Megalesian Incantation",
"A Delightful Sacrament",
"Thiatic Invocation",
"Formulae Voluptuous",
"Anthis Elaboration",
"Devouring Mysteries"
],
[Aspect.Moth]: [
"A Barber's Warning",
"A Wood-Whisper",
"An Ecdysiast's Parable",
"Moldywarp Admonitions",
"Centipede Testament",
"Thigh-born Thorax-Sweet",
"Mare-in-the-Yew"
],
[Aspect.Knock]: [
"A Locksmith's Secret",
"An Iguvine Rite",
"A Consent of Wounds",
"Mensicate Invocation",
"Formulae Ophidian",
"Liminal Evocation",
"Mysteries of Opening"
],
[Aspect.SecretHistories]: [
"An Occult Scrap",
"A Furtive Truth",
"A Forgotten Chronicle",
"A Forbidden Epic",
"Unresolved Ambiguity",
"Vagabond's Map",
"Port Noon Anecdote"
]
}
const Lore:
() => Card[]
=
() => {
const o: Card[] = [];
((Object as any).entries as (v: any) => [Principle, string[]][])(LoreNames).forEach(
([principle, names]) => names.map((name, i) => o.push({
...Card,
name,
aspects: {
[Aspect.Lore]: 1,
[principle]: (1+i)*2
}
}))
)
return o;
}
;
const InfluenceNames: Omit<Record<Principle,
readonly string[] & {
readonly [n in 0 | 1 | 2 | 3]: string
}
>, Aspect.SecretHistories> = {
[Aspect.Lantern]: [
"A Splendour",
"A Blaze of Radiance",
"An Intensity of Radiance",
"A Consciousness of Radiance",
],
[Aspect.Forge]: [
"An Incandescence",
"An Exalting Heat",
"A Trembling Heat",
"A Rising Heat"
],
[Aspect.Edge]: [
"A Resolution",
"A Furious Air",
"A Rousing Air",
"A Thrilling Air"
],
[Aspect.Winter]: [
"Perfect Frost",
"A Bitter Atmosphere",
"An Icy Atmosphere",
"A Chilly Atmosphere"
],
[Aspect.Heart]: [
"An Imminence",
"Pounding Airs",
"Pulsing Airs",
"Trembling Airs"
],
[Aspect.Grail]: [
"An Incarnadescence",
"An Imperative of Appetite",
"An Urgency of Appetite",
"An Awareness of Appetite"
],
[Aspect.Moth]: [
"That Old Yearning",
"A Howling in the Heart",
"A Rattling In the Soul",
"A Buzzing in the Brain"
],
[Aspect.Knock]: [
"Wrong Door",
"Subtle Rapture",
"Subtle Fracture",
"Subtle Flaw"
]
} as const;
const PrincipleInfluences:
() => Card[]
=
() => {
const o: Card[] = [];
((Object as any).entries as (v: any) => [Principle, string[]][])(InfluenceNames).forEach(
([principle, names]) => names.map((name, i) => o.push({
...Card,
name,
aspects: {
[Aspect.Influence]: 1,
[principle]: [15,10,6,2][i]
}
}))
)
return o;
}
;
interface Spirit extends Card {
aspects: Card["aspects"] & {
[Aspect.Spirit]: number,
[Aspect.Follower]: number
}
}
const Spirit = {
...Card,
aspects: {
[Aspect.Follower]: 1,
[Aspect.Spirit]: 1
}
} as const;
const Spirits: Spirit[] = [
{
...Spirit,
name: "Shattered Risen",
aspects: {
...Spirit.aspects,
[Aspect.Winter]: 4,
[Aspect.Edge]: 4,
}
},
{
...Spirit,
name: "Burgeoning Risen",
aspects: {
...Spirit.aspects,
[Aspect.Winter]: 4,
[Aspect.Moth]: 4
}
},
{
...Spirit,
name: "Voiceless Dead",
aspects: {
...Spirit.aspects,
[Aspect.Winter]: 6,
[Aspect.Moth]: 6
}
},
{
...Spirit,
name: "Hint",
aspects: {
...Spirit.aspects,
[Aspect.Lantern]: 8,
[Aspect.Edge]: 8
}
},
{
...Spirit,
name: "Percussigant",
aspects: {
...Spirit.aspects,
[Aspect.Heart]: 8,
[Aspect.Edge]: 8
}
},
{
...Spirit,
name: "Calgine",
aspects: {
...Spirit.aspects,
[Aspect.Forge]: 8,
[Aspect.Moth]: 8
}
},
{
...Spirit,
name: "Raw Prophet",
aspects: {
...Spirit.aspects,
[Aspect.Grail]: 8,
[Aspect.Moth]: 8
}
},
{
...Spirit,
name: "Maid-in-the-Mirror",
aspects: {
...Spirit.aspects,
[Aspect.Winter]: 10,
[Aspect.Edge]: 10
}
},
{
...Spirit,
name: "King Crucible",
aspects: {
...Spirit.aspects,
[Aspect.Forge]: 12,
[Aspect.Edge]: 12,
[Aspect.DeepMandicSpeaker]: 1
}
},
{
...Spirit,
name: "Ezeem, the Second Thirstly",
aspects: {
...Spirit.aspects,
[Aspect.Grail]: 12,
[Aspect.Edge]: 12,
[Aspect.PhrygianSpeaker]: 1
}
},
{
...Spirit,
name: "Ezeem, the Second Thirstly",
aspects: {
...Spirit.aspects,
[Aspect.Lantern]: 12,
[Aspect.SecretHistories]: 12,
[Aspect.FucineSpeaker]: 1
}
},
]
const cards: Card[] = [
...Lore(), ...PrincipleInfluences(), ...followers, ...Spirits,
{
...Card,
name: "Fleeting Reminiscence",
aspects: {
[Aspect.Moth]: 2,
[Aspect.Influence]: 1,
[Aspect.Memory]: 1,
[Aspect.SecretHistories]: 2
}
},
{
...Card,
name: "Restlessness",
aspects: {
[Aspect.Lantern]: 2,
[Aspect.Forge]: 2,
[Aspect.Heart]: 2,
[Aspect.Moth]: 2,
[Aspect.Grail]: 2,
[Aspect.Memory]: 1
}
},
{
...Card,
name: "A Human Corpse",
aspects: {
[Aspect.Grail]: 3,
[Aspect.Winter]: 3,
[Aspect.Ingredient]: 1,
[Aspect.Remnant]: 1
},
},
{
...Card,
name: "Fascination",
aspects: {
[Aspect.Moth]: 2,
[Aspect.IllHealth]: 1,
[Aspect.Memory]: 1
}
},
{
...Card,
name: "Dread",
aspects: {
[Aspect.Edge]: 2
}
},
{
...Card,
name: "Contentment",
aspects: {
[Aspect.Heart]: 2,
[Aspect.Lantern]: 2
}
}
];
interface Recipe extends Aspected, Named, Described {
readonly kind: Kind.Recipe
}
const Recipe = {
kind: Kind.Recipe
} as const;
interface Ritual extends Recipe { }
const Ritual = {
...Recipe
} as const;
const RitualSummonHint: Ritual = {
...Ritual,
name: "Summon a sharp-edged Lantern-thing",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Edge]: 2,
[Aspect.Lantern]: 6
}
}
const RitualSummonRedProphet: Ritual = {
...Ritual,
name: "Summon one of the more bewildering creatures of the Red Grail",
aspects: {
[Aspect.Grail]: 8,
[Aspect.Moth]: 2,
[Aspect.Knock]: 2
}
};
const Rituals: Ritual[] = [
RitualSummonRedProphet,
RitualSummonHint,
{
...Ritual,
name: "Call on the Cartographer of Scars to raise a corpse to half-life",
aspects: {
[Aspect.Remnant]: 1,
[Aspect.Edge]: 2,
[Aspect.Winter]: 4
}
},
{
...Ritual,
name: "Call on the Ring-Yew to raise a corpse to half-life",
aspects: {
[Aspect.Remnant]: 1,
[Aspect.Moth]: 2,
[Aspect.Winter]: 4
}
},
/*{
...Ritual,
name: "Call on the Ring-Yew to renew a corpse's half-life",
aspects: {
[Aspect.Moth]: 2,
[Aspect.Winter]: 4
}
}*/
{
...Ritual,
name: "Summon one of the Name-emanations of the Red Grail",
aspects: {
[Aspect.Knock]: 5,
[Aspect.Grail]: 10,
[Aspect.Forge]: 2
}
},
{
...Ritual,
name: "Summon the mysterious Name known as the Baldomerian",
aspects: {
[Aspect.Knock]: 5,
[Aspect.SecretHistories]: 2,
[Aspect.Lantern]: 10
}
},
{
...Ritual,
name: "Summon a creature of smoky deception",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Winter]: 2,
[Aspect.Forge]: 6
}
},
{
...Ritual,
name: "Summon a creature of the Thunderskin",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Edge]: 2,
[Aspect.Heart]: 6
}
},
{
...Ritual,
name: "Summon a servant of the Sun-in-Rags",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Winter]: 8,
[Aspect.Edge]: 2
}
},
{
...Ritual,
name: "Summon one of the Voiceless Dead",
aspects: {
[Aspect.Knock]: 2,
[Aspect.Winter]: 4,
[Aspect.Heart]: 2
}
},
{
...Ritual,
name: "Forge's Redemption",
aspects: {
[Aspect.Forge]: 15,
[Aspect.Grail]: 1
}
}
];
type ArrayOf<T> = T extends (infer K)[] ? K : never;
type ArrayOfAtLeastLengthOne<T> = [T, ...T[]];
declare interface Object {
entries<A extends string | number | symbol,B>(object: Partial<Record<A,B>>): [A extends string? A: string,B][]
}
const visitPermute:
<T>( f: ((opt: T[]) => unknown ), lists: ArrayOfAtLeastLengthOne<T[]>) => void
=
(f, [l, ...etc]) => {
if (etc.length == 0) return l.forEach(v => f([v]));
for (const v of l)
visitPermute((opt) => f([v, ...opt]), etc as ArrayOfAtLeastLengthOne<ArrayOf<typeof etc>>);
return;
}
;
const totalAspects:
(entities: Aspected[]) => Aspected["aspects"]
=
e => e.reduce((a, c, i) => {
for (const [aspect, magnitude] of Object.entries(c.aspects))
a[aspect as any as Aspect] = (a[aspect as any as Aspect] ?? 0) + magnitude;
return a;
}, {} as Aspected["aspects"])
;
const aspectFilterMatches:
(filter: Aspected["aspects"], test: Aspected["aspects"]) => boolean
=
(filter, test) => Object.entries(filter).every(
([aspect, minMagnitude]) => test[aspect as any as Aspect]! >= minMagnitude
)
;
const anyAspectFilterMatches:
(filter: Aspected["aspects"], test: Aspected["aspects"]) => boolean
=
(filter, test) => Object.entries(filter).some(
([aspect, minMagnitude]) => test[aspect as any as Aspect]! >= minMagnitude
)
;
declare interface Object {
fromEntries(...a: any): any
};
const StringifyAspects:
(aspects: Aspected["aspects"]) => Record<string, string>
=
aspects => Object.fromEntries(Object.entries(aspects).map(([aspect,mag]) => [Aspect[aspect as any as Aspect], mag])))
;
const RitualWith:
({ cards, ritual, rite }: { cards: Card[], ritual: Ritual, rite: Rite }) => Card[][]
=
({ cards, ritual: { aspects: aspectsNeeded }, rite: { slots } }) => {
const validCardsForSlot = slots.map(slot =>
cards.filter(card => {
return anyAspectFilterMatches(slot.aspects, card.aspects);
})
);
const validChoices: Card[][] = [];
visitPermute(cards => {
if (aspectFilterMatches(aspectsNeeded, totalAspects(cards))) validChoices.push(cards);
}, validCardsForSlot as [Card[], ...Card[][]]);
return validChoices;
}
;
const allowed = Rituals.map(ritual => ({
ritual: ritual, choices: RitualWith({
cards, ritual, rite: SunsetRite
})
})).filter(({ choices }) => choices.length > 0);
let s = `You may perform the following rituals:\n\n`;
for (const { ritual, choices } of allowed) {
s += `${ritual.name}\n`;
s += `${"=".repeat([...ritual.name || ""].length)}\n`;
s += `With the following choices:\n`
for (let i = 0; i < choices.length; i++) {
s += `\t${i+1}.\n`;
for (const { name } of choices[i]) {
s += `\t\t${name}\n`
}
}
s += "\n";
}
console.log(s);;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment