diff options
-rw-r--r-- | play.html | 52 | ||||
-rw-r--r-- | play.js | 47 | ||||
-rw-r--r-- | rules.js | 453 |
3 files changed, 328 insertions, 224 deletions
@@ -41,6 +41,11 @@ body.Opposition header.your_turn { background-color: hsl(16 85% 75%);; } #log .h1 { background-color: silver; } #log .h2 { background-color: gainsboro; } +#log .suf { background-color: hsl(273 39% 85%); } +#log .opp { background-color: hsl(16 85% 85%); } +#log .tip { cursor: pointer; } +#log .tip:hover { text-decoration: underline; } + #log > .p { padding-left: 20px; text-align: right; font-style: italic; opacity: 75%; } #log > .i { padding-left: 20px; } @@ -55,10 +60,10 @@ body.Opposition header.your_turn { background-color: hsl(16 85% 75%);; } gap: 20px; } -#hand, #discard, #states_draw, #strategy_draw, #set_aside { min-height: 350px; } -#hand_panel, #states_draw { min-width: 1100px; } -#strategy_draw, #set_aside_panel { min-width: 830px; } -#discard_panel { min-width: 290px; } +#hand, #discard, #support_claimed, #support_discard, #opposition_claimed, #opposition_discard, #states_draw, #strategy_draw, #out_of_play { min-height: 350px; } +#hand_panel, #states_draw, #support_discard_panel, #opposition_discard_panel, #out_of_play { min-width: 1100px; } +#strategy_draw { min-width: 790px; } +#support_claimed, #opposition_claimed { min-width: 290px; } .panel { background-color: #555; @@ -886,45 +891,58 @@ c5 3 13 7 17 8 8 2 9 3 11 12 1 5 5 12 8 16 5 8 5 8 3 22 l-3 14 -30 -1 c-35 </div> <div class="panel_grid"> - <div id="hand_panel" class="panel hide"> <div id="hand_header" class="panel_header">Hand</div> <div id="hand" class="panel_body"></div> </div> + </div> - <div id="discard_panel" class="panel"> - <div id="discard_header" class="panel_header">Discard</div> - <div id="discard" class="panel_body"></div> + <div class="panel_grid"> + <div id="support_claimed_panel" class="panel"> + <div id="support_claimed_header" class="panel_header">Suffragist Claimed</div> + <div id="support_claimed" class="panel_body"></div> </div> + <div id="opposition_claimed_panel" class="panel"> + <div id="opposition_claimed_header" class="panel_header">Opposition Claimed</div> + <div id="opposition_claimed" class="panel_body"></div> + </div> </div> <div class="panel_grid"> - <div id="strategy_draw_panel" class="panel"> - <div id="strategy_draw_header" class="panel_header">Strategy cards</div> + <div id="strategy_draw_header" class="panel_header">Available Strategy cards</div> <div id="strategy_draw" class="panel_body"></div> </div> </div> <div class="panel_grid"> - <div id="states_draw_panel" class="panel"> - <div id="states_draw_header" class="panel_header">States cards</div> + <div id="states_draw_header" class="panel_header">Available States cards</div> <div id="states_draw" class="panel_body"></div> </div> - </div> - <div class="panel_grid"> + <div id="support_discard_panel" class="panel"> + <div id="support_discard_header" class="panel_header">Suffragist Discard</div> + <div id="support_discard" class="panel_body"></div> + </div> + </div> - <div id="set_aside_panel" class="panel hide"> - <div id="set_aside_header" class="panel_header">Set-aside cards</div> - <div id="set_aside" class="panel_body"></div> + <div class="panel_grid"> + <div id="opposition_discard_panel" class="panel"> + <div id="opposition_discard_header" class="panel_header">Opposition Discard</div> + <div id="opposition_discard" class="panel_body"></div> </div> + </div> + <div class="panel_grid"> + <div id="out_of_play_panel" class="panel"> + <div id="out_of_play_header" class="panel_header">Out of Play</div> + <div id="out_of_play" class="panel_body"></div> + </div> </div> <br><br><br> @@ -274,13 +274,19 @@ function on_log(text) { // eslint-disable-line no-unused-vars text = text.substring(4) p.className = 'h1' } - - if (text.match(/^\.h2/)) { + else if (text.match(/^\.h2/)) { text = text.substring(4) p.className = 'h2' } - - if (text.match(/^\.h3/)) { + else if (text.match(/^\.h3.suf/)) { + text = text.substring(8) + p.className = 'h3 suf' + } + else if (text.match(/^\.h3.opp/)) { + text = text.substring(8) + p.className = 'h3 opp' + } + else if (text.match(/^\.h3/)) { text = text.substring(4) p.className = 'h3' } @@ -289,16 +295,15 @@ function on_log(text) { // eslint-disable-line no-unused-vars return p } +const pluralize = (count, noun, suffix = 's') => + `${count} ${noun}${count !== 1 ? suffix : ''}`; + function support_info() { - if (view.support_hand === 1) - return "1 card in hand"; - return view.support_hand + " cards in hand"; + return `${pluralize(view.support_buttons, 'button')}, ${pluralize(view.support_hand, 'card')} in hand` } function opposition_info() { - if (view.opposition_hand === 1) - return "1 card in hand"; - return view.opposition_hand + " cards in hand"; + return `${pluralize(view.opposition_buttons, 'button')}, ${pluralize(view.opposition_hand, 'card')} in hand` } function on_update() { // eslint-disable-line no-unused-vars @@ -311,8 +316,13 @@ function on_update() { // eslint-disable-line no-unused-vars document.getElementById("opposition_info").textContent = opposition_info() document.getElementById("hand").replaceChildren() + document.getElementById("support_claimed").replaceChildren() + document.getElementById("support_discard").replaceChildren() + document.getElementById("opposition_claimed").replaceChildren() + document.getElementById("opposition_discard").replaceChildren() document.getElementById("states_draw").replaceChildren() document.getElementById("strategy_draw").replaceChildren() + document.getElementById("out_of_play").replaceChildren() if (view.hand) { document.getElementById("hand_panel").classList.remove("hide") @@ -322,11 +332,28 @@ function on_update() { // eslint-disable-line no-unused-vars document.getElementById("hand_panel").classList.add("hide") } + for (let c of view.support_claimed) + document.getElementById("support_claimed").appendChild(ui.cards[c]) + for (let c of view.support_discard) + document.getElementById("support_discard").appendChild(ui.cards[c]) + for (let c of view.opposition_claimed) + document.getElementById("opposition_claimed").appendChild(ui.cards[c]) + for (let c of view.opposition_discard) + document.getElementById("opposition_discard").appendChild(ui.cards[c]) + for (let c of view.states_draw) document.getElementById("states_draw").appendChild(ui.cards[c]) for (let c of view.strategy_draw) document.getElementById("strategy_draw").appendChild(ui.cards[c]) + for (let c of view.out_of_play) + document.getElementById("out_of_play").appendChild(ui.cards[c]) + + action_button("commit_1_button", "+1 Button") + action_button("defer", "Defer") + action_button("match", "Match") + action_button("supersede", "Supersede") + action_button("draw", "Draw") action_button("skip", "Skip") @@ -18,9 +18,111 @@ const last_strategy_card = 116 const first_states_card = 117 const last_states_card = 128 +// #region CARD & HAND FUNCTIONS + +function is_support_card(c) { + return c >= first_support_card && c <= last_support_card +} + +function is_opposition_card(c) { + return c >= first_opposition_card && c <= last_opposition_card +} + +function is_strategy_card(c) { + return c >= first_strategy_card && c <= last_strategy_card +} + +function is_states_card(c) { + return c >= first_states_card && c <= last_states_card +} + +function draw_card(deck) { + if (deck.length === 0) + throw Error("can't draw from empty deck") + return deck.pop() +} + +function player_hand() { + if (game.active === SUF) { + return game.support_hand + } else if (game.active === OPP) { + return game.opposition_hand + } + return [] +} + +function player_claimed() { + if (game.active === SUF) { + return game.support_claimed + } else { + return game.opposition_claimed + } +} + +function player_discard() { + if (game.active === SUF) { + return game.support_discard + } else { + return game.opposition_discard + } +} + +function is_player_claimed_card(c) { + return player_claimed().includes(c) +} + +// #region PUBLIC FUNCTIONS + exports.scenarios = [ "Standard" ] exports.roles = [ SUF, OPP ] +function gen_action(action, argument) { + if (argument === undefined) { + view.actions[action] = 1 + } else { + if (!(action in view.actions)) + view.actions[action] = [] + view.actions[action].push(argument) + } +} + +exports.action = function (state, player, action, arg) { + game = state + if (states[game.state] && action in states[game.state]) { + states[game.state][action](arg, player) + } else { + if (action === "undo" && game.undo && game.undo.length > 0) + pop_undo() + else + throw new Error("Invalid action: " + action) + } + return game +} + +exports.resign = function (state, player) { + game = state + if (game.state !== "game_over") { + if (player === SUF) + goto_game_over(OPP, "Suffragist resigned.") + if (player === OPP) + goto_game_over(SUF, "Opposition resigned.") + } + return game +} + +function goto_game_over(result, victory) { + game.state = "game_over" + game.active = "None" + game.result = result + game.victory = victory + log_br() + log(game.victory) +} + +// #endregion + +// #region SETUP + exports.setup = function (seed, _scenario, _options) { game = { seed: seed, @@ -56,6 +158,8 @@ exports.setup = function (seed, _scenario, _options) { opposition_claimed: [], opposition_campaigner: [0, 0], opposition_buttons: 0, + + out_of_play: [] } log_h1("Votes for Women") @@ -81,7 +185,7 @@ function setup_game() { shuffle(game.states_draw) shuffle(game.strategy_deck) - game.states_draw.splice(-3) // 3 states card aren't used + game.out_of_play.push(...game.states_draw.splice(-3)) // 3 states card aren't used log_h2("States Cards") for (let c of game.states_draw) log(`C${c}`) @@ -109,12 +213,88 @@ function init_player_cards(first_card) { return [].concat(late, middle, early) } -function draw_card(deck) { - if (deck.length === 0) - throw Error("can't draw from empty deck") - return deck.pop() +// #endregion + +// #region VIEW + +exports.view = function(state, player) { + game = state + + view = { + log: game.log, + active: game.active, + prompt: null, + actions: null, + + turn: game.turn, + round: game.round, + congress: game.congress, + states: game.states, + + strategy_deck: game.strategy_deck.length, + strategy_draw: game.strategy_draw, + states_draw: game.states_draw, + + persisted_turn: game.persisted_turn, + persisted_game: game.persisted_game, + persisted_ballot: game.persisted_ballot, + + support_deck: game.support_deck.length, + support_discard: game.support_discard, // top_discard? + support_hand: game.support_hand.length, + support_claimed: game.support_claimed, + purple_campaigner: game.purple_campaigner, + yellow_campaigner: game.yellow_campaigner, + support_buttons: game.support_buttons, + + opposition_deck: game.opposition_deck.length, + opposition_discard: game.opposition_discard, // top_discard? + opposition_hand: game.opposition_hand.length, + opposition_claimed: game.opposition_claimed, + opposition_campaigner: game.opposition_campaigner, + opposition_buttons: game.opposition_buttons, + + out_of_play: game.out_of_play, + + hand: 0, + } + + if (player === SUF) { + view.hand = game.support_hand + } else if (player === OPP) { + view.hand = game.opposition_hand + } + + if (game.state === "game_over") { + view.prompt = game.victory + } else if (player === "Observer" || (game.active !== player && game.active !== "Both")) { + if (states[game.state]) { + let inactive = states[game.state].inactive + view.prompt = `Waiting for ${game.active} to ${inactive}...` + } else { + view.prompt = "Unknown state: " + game.state + } + } else { + view.actions = {} + if (states[game.state]) + states[game.state].prompt(player) + else + view.prompt = "Unknown state: " + game.state + if (view.actions.undo === undefined) { + if (game.undo && game.undo.length > 0) + view.actions.undo = 1 + else + view.actions.undo = 0 + } + } + + return view } +// #endregion + +// #region FLOW OF PLAY + function start_turn() { game.turn += 1 log_h1("Turn " + game.turn) @@ -129,7 +309,7 @@ function goto_planning_phase() { } states.planning_phase = { - inactive: "to do Planning.", + inactive: "do Planning.", prompt() { view.prompt = "Planning." gen_action("draw") @@ -167,12 +347,12 @@ function end_planning_phase() { } function goto_strategy_phase() { - log_h2("Strategy Phase") + log_h2("Strategy") game.state = "strategy_phase" } states.strategy_phase = { - inactive: "to do Strategy.", + inactive: "do Strategy.", prompt() { view.prompt = "Strategy: TODO" gen_action("done") @@ -183,41 +363,13 @@ states.strategy_phase = { } function goto_operations_phase() { - log_h2("Operations Phase") + log_h2("Operations") game.state = "operations_phase" game.active = SUF game.round = 1 begin_player_round() } -function player_hand() { - if (game.active === SUF) { - return game.support_hand - } else if (game.active === OPP) { - return game.opposition_hand - } - return [] -} - -function player_claimed() { - if (game.active === SUF) { - return game.support_claimed - } else { - return game.opposition_claimed - } -} - -function player_discard() { - if (game.active === SUF) { - return game.support_discard - } else { - return game.opposition_discard - } -} - -function is_player_claimed_card(c) { - return player_claimed().includes(c) -} function count_player_active_campaigners() { if (game.active === SUF) { @@ -240,6 +392,7 @@ function after_play_card(c) { game.has_played_claimed = 1 // remove from game array_remove_item(player_claimed(), c) + game.out_of_play.push(c) } else { game.has_played_hand = 1 // move to discard @@ -248,30 +401,27 @@ function after_play_card(c) { } } -function gen_card_actions(c) { - gen_action("card_event", c) - if (has_player_active_campaigners()) { - gen_action("card_campaigning", c) - gen_action("card_organizing", c) - gen_action("card_lobbying", c) - } -} - states.operations_phase = { - inactive: "to do Operations Phase", + inactive: "Play a Card.", prompt() { - view.prompt = "Operations: Play a Card" + view.prompt = "Operations: Play a Card." if (!game.has_played_hand) { for (let c of player_hand()) { - gen_card_actions(c) + gen_action("card_event", c) + if (has_player_active_campaigners()) { + gen_action("card_campaigning", c) + gen_action("card_organizing", c) + gen_action("card_lobbying", c) + } } } if (!game.has_played_claimed) { // only one claimed can be played per turn for (let c of player_claimed()) { - gen_card_actions(c) + // TODO is this the right type of event? + gen_action("card_event", c) } } if (game.has_played_hand) @@ -279,22 +429,25 @@ states.operations_phase = { }, card_event(c) { push_undo() - log(`Playing C${c} as Event`) + log(`Played C${c} as Event`) after_play_card(c) + + // XXX auto-done to speed-up testing + end_player_round() }, card_campaigning(c) { push_undo() - log(`Playing C${c} for Campaigning Action`) + log(`Played C${c} for Campaigning Action`) after_play_card(c) }, card_organizing(c) { push_undo() - log(`Playing C${c} for Organizing Action`) + log(`Played C${c} for Organizing Action`) after_play_card(c) }, card_lobbying(c) { push_undo() - log(`Playing C${c} for Lobbying Action`) + log(`Played C${c} for Lobbying Action`) after_play_card(c) }, done() { @@ -303,7 +456,7 @@ states.operations_phase = { } function begin_player_round() { - log_h3(`${game.active} Round ${game.round}`) + log_round(`Round ${game.round}`) } function end_player_round() { @@ -326,14 +479,14 @@ function end_player_round() { } function goto_cleanup_phase() { - log_h2("Cleanup Phase") + log_h2("Cleanup") game.state = "cleanup_phase" } states.cleanup_phase = { - inactive: "to do Cleanup.", + inactive: "do Cleanup.", prompt() { - view.prompt = "Cleanup: TODO" + view.prompt = "Cleanup." gen_action("done") }, done() { @@ -341,14 +494,32 @@ states.cleanup_phase = { } } +function cleanup_persistent_turn_cards() { + // any cards in the “Cards in Effect for the Rest of the Turn box” are placed in the appropriate discard pile. + for (let c of game.persisted_turn) { + if (is_support_card(c)) { + game.support_discard.push(c) + } else if (is_opposition_card(c)) { + game.opposition_discard.push(c) + } else { + throw Error(`Unexpected card ${c} on persisted_turn`) + } + } + game.persisted_turn = [] +} + function end_cleanup_phase() { - // TODO - // At the end of Turns 1-5, any cards in the “Cards - // in Effect for the Rest of the Turn box” are placed - // in the appropriate discard pile. Each player should - // have one card in their hand to carry over to the next - // Turn. The Turn marker is advanced and the - // next Turn begins. + if (game.turn < 6) { + cleanup_persistent_turn_cards() + + if (game.support_hand.length !== 1) + throw Error("ASSERT game.support_hand.length === 1") + if (game.opposition_hand.length !== 1) + throw Error("ASSERT game.opposition_hand.length === 1") + + start_turn() + return + } // TODO // At the end of Turn 6, if the Nineteenth @@ -362,139 +533,12 @@ function end_cleanup_phase() { // Effect for the Rest of the Game box” are placed in // the appropriate discard pile. - if (game.support_hand.length !== 1) - throw Error("ASSERT game.support_hand.length === 1") - if (game.opposition_hand.length !== 1) - throw Error("ASSERT game.opposition_hand.length === 1") - - if (game.turn < 6) { - start_turn() - } else { - goto_game_over(OPP, "Opposition wins.") - } + goto_game_over(OPP, "Opposition wins.") } -// public functions +// #endregion -function gen_action(action, argument) { - if (argument === undefined) { - view.actions[action] = 1 - } else { - if (!(action in view.actions)) - view.actions[action] = [] - view.actions[action].push(argument) - } -} - -exports.action = function (state, player, action, arg) { - game = state - if (states[game.state] && action in states[game.state]) { - states[game.state][action](arg, player) - } else { - if (action === "undo" && game.undo && game.undo.length > 0) - pop_undo() - else - throw new Error("Invalid action: " + action) - } - return game -} - -exports.resign = function (state, player) { - game = state - if (game.state !== "game_over") { - if (player === SUF) - goto_game_over(OPP, "Suffragist resigned.") - if (player === OPP) - goto_game_over(SUF, "Opposition resigned.") - } - return game -} - -function goto_game_over(result, victory) { - game.state = "game_over" - game.active = "None" - game.result = result - game.victory = victory - log_br() - log(game.victory) -} - -// === VIEW === - -exports.view = function(state, player) { - game = state - - view = { - log: game.log, - active: game.active, - prompt: null, - actions: null, - - turn: game.turn, - round: game.round, - congress: game.congress, - states: game.states, - - strategy_deck: game.strategy_deck.length, - strategy_draw: game.strategy_draw, - states_draw: game.states_draw, - - persisted_turn: game.persisted_turn, - persisted_game: game.persisted_game, - persisted_ballot: game.persisted_ballot, - - support_deck: game.support_deck.length, - support_discard: game.support_discard, // top_discard? - support_hand: game.support_hand.length, - support_claimed: game.support_claimed, - purple_campaigner: game.purple_campaigner, - yellow_campaigner: game.yellow_campaigner, - support_buttons: game.support_buttons, - - opposition_deck: game.opposition_deck.length, - opposition_discard: game.opposition_discard, // top_discard? - opposition_hand: game.opposition_hand.length, - opposition_claimed: game.opposition_claimed, - opposition_campaigner: game.opposition_campaigner, - opposition_buttons: game.opposition_buttons, - - hand: 0, - } - - if (player === SUF) { - view.hand = game.support_hand - } else if (player === OPP) { - view.hand = game.opposition_hand - } - - if (game.state === "game_over") { - view.prompt = game.victory - } else if (player === "Observer" || (game.active !== player && game.active !== "Both")) { - if (states[game.state]) { - let inactive = states[game.state].inactive - view.prompt = `Waiting for ${game.active} to ${inactive}...` - } else { - view.prompt = "Unknown state: " + game.state - } - } else { - view.actions = {} - if (states[game.state]) - states[game.state].prompt(player) - else - view.prompt = "Unknown state: " + game.state - if (view.actions.undo === undefined) { - if (game.undo && game.undo.length > 0) - view.actions.undo = 1 - else - view.actions.undo = 0 - } - } - - return view -} - - -// === LOGGING === +// #region LOGGING function log(msg) { game.log.push(msg) @@ -526,11 +570,22 @@ function log_h3(msg) { log(".h3 " + msg) } +function log_round(msg) { + log_br() + if (game.active === SUF) + log(".h3.suf " + msg) + else + log(".h3.opp " + msg) + log_br() +} + function log_sep() { log(".hr") } -// === COMMON LIBRARY === +// #endregion + +// #region COMMON LIBRARY function clear_undo() { if (game.undo.length > 0) @@ -620,4 +675,8 @@ function array_remove(array, index) { array.length = n - 1 } -// === GENERATED EVENT CODE === +// #endregion + +// #region GENERATED EVENT CODE + +// #endregion |