summaryrefslogtreecommitdiff
path: root/rules.ts
diff options
context:
space:
mode:
Diffstat (limited to 'rules.ts')
-rw-r--r--rules.ts423
1 files changed, 423 insertions, 0 deletions
diff --git a/rules.ts b/rules.ts
new file mode 100644
index 0000000..a2a71ca
--- /dev/null
+++ b/rules.ts
@@ -0,0 +1,423 @@
+'use strict';
+
+import data, {
+ // LIBERTY,
+ // COLLECTIVIZATION,
+ // GOVERNMENT,
+ // SOVIET_SUPPORT,
+ // FOREIGN_AID,
+ // MORALE_BONUS,
+ // TEAMWORK_BONUS,
+ // OFF,
+ ON,
+ EventCard,
+ // StaticData,
+ // PLAYER_WITH_MOST_HERO_POINTS,
+} from './data';
+
+declare const brand: unique symbol;
+
+// branded typing
+type Brand<T, TBrand extends string> = T & {
+ [brand]: TBrand;
+};
+export type CardId = Brand<number, 'CardId'>;
+export type Player = Brand<string, 'Player'>;
+export type FactionId = Brand<string, 'FactionId'>;
+
+interface Game {
+ [index: number]: any;
+ seed: number;
+ log: number | string[];
+ undo: Game[];
+ turn: number;
+ year: number;
+ active: Player | null;
+ state: string | null;
+ bag_of_glory: Record<FactionId, number>;
+ bonuses: number[];
+ current_events: CardId[];
+ fronts: {
+ a: number;
+ m: number;
+ n: number;
+ s: number;
+ };
+ hands: Record<FactionId, CardId[]>;
+ hero_points: Record<FactionId, number>;
+ initiative: FactionId;
+ tracks: number[];
+
+ result?: string;
+ victory?: string;
+
+ location?: string;
+ selected?: string;
+
+ // played_card: CardId
+
+ // turn: Turn
+}
+
+export interface View {
+ log: number | string[];
+ active?: string | null;
+ prompt: string | null;
+ actions?: any;
+ victory?: string;
+ location?: string;
+ selected?: string;
+
+ current_events: CardId[];
+ hand: CardId[];
+}
+
+// interface State {
+// inactive: string;
+// prompt: () => void;
+// }
+
+type States = {
+ [key: string]: any;
+};
+
+const states = {} as States;
+let game = {} as Game; // = null
+var view = {} as View; // = null
+
+const ANARCHIST = 'Anarchist' as Player;
+const COMMUNIST = 'Communist' as Player;
+const MODERATE = 'Moderate' as Player;
+
+const ANARCHISTS_ID = 'a' as FactionId;
+const COMMUNISTS_ID = 'c' as FactionId;
+const MODERATES_ID = 'm' as FactionId;
+
+const {
+ cards,
+ // fronts
+} = data;
+
+const faction_cards = {
+ [ANARCHIST]: make_list(37, 54) as CardId[],
+ [COMMUNIST]: make_list(19, 36) as CardId[],
+ [MODERATE]: make_list(1, 18) as CardId[],
+};
+
+const fascist_decks = {
+ 1: make_list(55, 62),
+};
+
+export const scenarios = ['Standard'];
+
+export const roles: Player[] = [ANARCHIST, COMMUNIST, MODERATE];
+
+function gen_action(action, argument) {
+ if (!(action in view.actions)) view.actions[action] = [];
+ view.actions[action].push(argument);
+}
+
+function gen_action_space(space) {
+ gen_action('space', space);
+}
+
+function gen_action_piece(piece) {
+ gen_action('piece', piece);
+}
+
+// function gen_action_card(card) {
+// gen_action('card', card);
+// }
+
+export function action(
+ state: any,
+ player: Player,
+ action: string,
+ arg: unknown
+) {
+ game = state;
+ let S = states[game.state];
+ if (action in S) S[action](arg, player);
+ else if (action === 'undo' && game.undo && game.undo.length > 0) pop_undo();
+ else throw new Error('Invalid action: ' + action);
+ return game;
+}
+
+// #region VIEW
+
+export { game_view as view };
+
+function game_view(state: Game, player: Player) {
+ console.log('game_view', state);
+ console.log('player', player);
+ game = state;
+
+ const faction_id = get_faction_id(player);
+
+ view = {
+ log: game.log,
+ prompt: null,
+ location: game.location,
+ selected: game.selected,
+ current_events: game.current_events,
+ hand: game.hands[faction_id],
+ };
+
+ if (player !== game.active) {
+ let inactive = states[game.state].inactive || game.state;
+ view.prompt = `Waiting for ${game.active} to ${inactive}.`;
+ } else {
+ view.actions = {};
+ states[game.state].prompt();
+ if (game.undo && game.undo.length > 0) view.actions.undo = 1;
+ else view.actions.undo = 0;
+ }
+
+ return view;
+}
+
+// #endregion
+
+// #region setup
+
+export function setup(seed: number, _scenario: string, _options: unknown) {
+ game = {
+ seed: seed,
+ state: null,
+ active: ANARCHIST,
+ bag_of_glory: {
+ [ANARCHISTS_ID]: 1,
+ [COMMUNISTS_ID]: 1,
+ [MODERATES_ID]: 1,
+ },
+ bonuses: [ON, ON],
+ current_events: [],
+ fronts: {
+ a: 2,
+ m: 2,
+ n: 2,
+ s: 2,
+ },
+ hands: {
+ [ANARCHISTS_ID]: [
+ draw_card(faction_cards[ANARCHIST]),
+ draw_card(faction_cards[ANARCHIST]),
+ draw_card(faction_cards[ANARCHIST]),
+ draw_card(faction_cards[ANARCHIST]),
+ draw_card(faction_cards[ANARCHIST]),
+ ],
+ [COMMUNISTS_ID]: [
+ draw_card(faction_cards[COMMUNIST]),
+ draw_card(faction_cards[COMMUNIST]),
+ draw_card(faction_cards[COMMUNIST]),
+ draw_card(faction_cards[COMMUNIST]),
+ draw_card(faction_cards[COMMUNIST]),
+ ],
+ [MODERATES_ID]: [
+ draw_card(faction_cards[MODERATE]),
+ draw_card(faction_cards[MODERATE]),
+ draw_card(faction_cards[MODERATE]),
+ draw_card(faction_cards[MODERATE]),
+ draw_card(faction_cards[MODERATE]),
+ ],
+ },
+ hero_points: {
+ [ANARCHISTS_ID]: 2,
+ [COMMUNISTS_ID]: 2,
+ [MODERATES_ID]: 0,
+ },
+ initiative: MODERATES_ID,
+ tracks: [5, 5, 6, 3, 3],
+ log: [],
+ undo: [],
+ turn: 1,
+ year: 1,
+ };
+
+ fascist_event();
+
+ game.state = 'move';
+
+ return game;
+}
+
+// #endregion
+
+states.move = {
+ inactive: 'move',
+ prompt() {
+ view.prompt = 'Move a piece.';
+ for (let p = 0; p < 32; ++p) gen_action_piece(p);
+ },
+ // piece(p) {
+ // game.selected = p
+ // game.state = "move_to"
+ // },
+};
+
+states.move_to = {
+ inactive: 'move',
+ prompt() {
+ view.prompt = 'Move the piece to a space.';
+ for (let s = 0; s < 64; ++s) gen_action_space(s);
+ },
+ // space(to) {
+ // game.location[game.selected] = to
+ // game.state = "move"
+ // if (game.active === PLAYER1)
+ // game.active = PLAYER2
+ // else
+ // game.active = PLAYER1
+ // },
+};
+
+function pop_undo() {
+ const save_log = game.log;
+ const save_undo = game.undo;
+ game = save_undo.pop()!;
+ (save_log as string[]).length = game.log as number;
+ game.log = save_log;
+ game.undo = save_undo;
+}
+
+// #region CARDS
+// function draw_faction_card(faction: Player): CardId {
+// return draw_faction_cards(faction, 1)[0];
+// }
+
+// function draw_faction_cards(faction: Player, count: number = 1): CardId[] {
+// const drawnCards = [];
+
+// }
+
+function draw_card(deck: CardId[]) {
+ clear_undo();
+ let i = random(deck.length);
+ let c = deck[i];
+ set_delete(deck, c);
+ return c;
+}
+
+// #endregion
+
+// #region EVENTS
+
+function resolve_event_attack(target: string | number, value: number) {
+ switch (target) {
+ case 'v':
+ break;
+ case 'd':
+ break;
+ default:
+ game.fronts[target] += value;
+ }
+}
+
+function fascist_event() {
+ const deck = fascist_decks[game.year];
+ const cardId = draw_card(deck);
+ game.current_events.push(cardId);
+
+ const card = cards[cardId] as EventCard;
+
+ card.effects.forEach((effect) => {
+ switch (effect.type) {
+ case 'attack':
+ resolve_event_attack(effect.target, effect.value);
+ break;
+ case 'bonus':
+ break;
+ case 'hero_points':
+ break;
+ case 'track':
+ game.tracks[effect.target] += effect.value;
+ break;
+ }
+ });
+}
+
+// #endregion
+
+// #region UTILITY
+
+function clear_undo() {
+ if (game.undo.length > 0) game.undo = [];
+}
+
+function get_faction_id(player: Player) {
+ switch (player) {
+ case ANARCHIST:
+ return ANARCHISTS_ID;
+ case COMMUNIST:
+ return COMMUNISTS_ID;
+ case MODERATE:
+ return MODERATES_ID;
+ default:
+ throw new Error('Unknown player');
+ }
+}
+
+function make_list(first: number, last: number) {
+ let list = [];
+ for (let i = first; i <= last; i++) list.push(i);
+ return list;
+}
+
+function random(range: number): number {
+ // An MLCG using integer arithmetic with doubles.
+ // https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf
+ // m = 2**35 − 31
+ return (game.seed = (game.seed * 200105) % 34359738337) % range;
+}
+
+function set_delete<T>(set: T[], item: T) {
+ let a = 0;
+ let b = set.length - 1;
+ while (a <= b) {
+ let m = (a + b) >> 1;
+ let x = set[m];
+ if (item < x) b = m - 1;
+ else if (item > x) a = m + 1;
+ else {
+ array_remove(set, m);
+ return;
+ }
+ }
+}
+
+// #endregion
+
+// #region ARRAY
+
+function array_remove<T>(array: T[], index: number) {
+ let n = array.length;
+ for (let i = index + 1; i < n; ++i) array[i - 1] = array[i];
+ array.length = n - 1;
+}
+
+// function array_insert<T>(array: T[], index: number, item: T) {
+// for (let i = array.length; i > index; --i) array[i] = array[i - 1];
+// array[index] = item;
+// }
+
+// function array_remove_pair<T>(array: T[], index: number) {
+// let n = array.length;
+// for (let i = index + 2; i < n; ++i) array[i - 2] = array[i];
+// array.length = n - 2;
+// }
+
+// function array_insert_pair<K, V>(
+// array: (K | V)[],
+// index: number,
+// key: K,
+// value: V
+// ) {
+// for (let i = array.length; i > index; i -= 2) {
+// array[i] = array[i - 2];
+// array[i + 1] = array[i - 1];
+// }
+// array[index] = key;
+// array[index + 1] = value;
+// }
+
+// #endregion