From 719b7f1cd0917980021f8715c7f5bf8a642254f6 Mon Sep 17 00:00:00 2001 From: Tor Andersson Date: Sun, 3 Nov 2024 01:57:36 +0100 Subject: Propose deals and list accepted deals in political display. --- play.css | 35 +++++++ play.html | 60 ++++++++++- play.js | 113 ++++++++++++++++++++- rules.js | 339 +++++++++++++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 473 insertions(+), 74 deletions(-) diff --git a/play.css b/play.css index 57459ea..5ad6c16 100644 --- a/play.css +++ b/play.css @@ -190,6 +190,41 @@ header.your_turn.austria { background-color: var(--color-light-austria); } --color-reserve: #59594c; } +/* DEALS */ + +dialog { + background-color: #f3ebd4; +} + +dialog button { + margin-left: 8px; +} + +#political_body table { + width: 100%; + background-color: var(--color-light-political); + border: 1px solid #0008; +} + +#political_body div.deal { + margin: 16px 8px 8px 8px; +} + +#political_body td { + border: 1px solid #0004; + padding: 4px; +} + +#political_body td img { + display: block; + border: 1px solid black; +} + +#political_body th { + font-weight: normal; + background-color: #0002; +} + /* PANELS */ .panel { diff --git a/play.html b/play.html index c5203ec..bd8cb5b 100644 --- a/play.html +++ b/play.html @@ -38,8 +38,10 @@ -
  • Propose subsidy contract -
  • Cancel subsidy contract +
  • Propose subsidy contract +
  • Cancel subsidy contract +
  • +
  • Propose deal
  • @@ -122,6 +124,8 @@
    +
    +
    @@ -141,4 +145,56 @@ + +
    + +
    + +

    + + +
    + +

    + + Last until: +
    + +

    + +
    + + +
    + +
    +
    + diff --git a/play.js b/play.js index 0decf5f..76aeb9e 100644 --- a/play.js +++ b/play.js @@ -92,6 +92,12 @@ const turn_name = [ "Winter 1744", ] +const DI_TURN = 0 +const DI_A_POWER = 1 +const DI_B_POWER = 2 +const DI_A_PROMISE = 3 +const DI_B_PROMISE = 4 + const F_EMPEROR_FRANCE = 1 const F_EMPEROR_AUSTRIA = 2 const F_ITALY_FRANCE = 4 @@ -1523,11 +1529,21 @@ function on_update() { layout_combat_marker() } - action_menu(document.getElementById("subsidy_menu"), [ + window.subsidy_menu.classList.toggle("hide", is_intro()) + action_menu(window.subsidy_menu, [ "propose_subsidy", "cancel_subsidy", + "propose_deal", ]) + update_deal_list(view.deals, window.active_deal_list, "Active Deals") + if (view.proposed_deal) { + update_deal_list([ view.proposed_deal ], window.proposed_deal_list, "Proposed Deal") + setTimeout(() => scroll_into_view(window.proposed_deal_list), 333) + } else { + update_deal_list(null, window.proposed_deal_list, "Proposed Deal") + } + action_button_with_argument("suit", SPADES, colorize_S) action_button_with_argument("suit", CLUBS, colorize_C) action_button_with_argument("suit", HEARTS, colorize_H) @@ -1679,6 +1695,101 @@ function on_log(text) { return p } +/* DEAL DIALOGS */ + +function propose_subsidy() { send_action("propose_subsidy") } +function cancel_subsidy() { send_action("cancel_subsidy") } + +function html_escape(str) { + return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") +} + +function update_deal_item(deal) { + let [ turn, a_power, b_power, a_promise, b_promise ] = deal + let str = "" + str += "" + power_image[a_power] + str += "" + html_escape(a_promise) + str += "" + power_image[b_power] + str += "" + html_escape(b_promise) + str += "" + turn_name[turn] + return str +} + +function update_deal_list(deals, elt, title) { + if (deals && deals.length > 0) { + let str = "
    " + title + for (let deal of deals) + str += update_deal_item(deal) + str += "
    " + elt.innerHTML = str + elt.style.display = "block" + } else { + elt.style.display = "none" + } +} + +function propose_deal() { + if (!is_action("propose_deal")) + return + + let form = window.propose_deal_form + + form.a_power.value = view.power + switch (view.power) { + case P_FRANCE: + form.b_power.value = P_PRUSSIA + break + case P_PRUSSIA: + form.b_power.value = P_FRANCE + break + case P_PRAGMATIC: + form.b_power.value = P_AUSTRIA + break + case P_AUSTRIA: + form.b_power.value = P_PRAGMATIC + break + } + form.turn.value = view.turn + + for (let opt of form.turn.options) + opt.disabled = (opt.value < view.turn) + + window.propose_deal_dlog.showModal() +} + +function propose_deal_submit(evt) { + evt.preventDefault() + + let data = Object.fromEntries(new FormData(window.propose_deal_form)) + + data.turn = Number(data.turn) + data.a_power = Number(data.a_power) + data.b_power = Number(data.b_power) + + if (!data.a_promise || !data.b_promise) { + alert("Each side must promise something!") + return + } + + console.log(data, player_from_power(data.a_power), player_from_power(data.b_power)) + if (player_from_power(data.a_power) === player_from_power(data.b_power)) { + alert("Cannot create deals between powers controlled by the same player.") + return + } + + let deal = [ data.turn, data.a_power, data.b_power, data.a_promise, data.b_promise ] + + if (view.actions.propose_deal) + send_message("action", [ "propose_deal", deal, game_cookie ]) + + window.propose_deal_dlog.close() +} + +function propose_deal_cancel(evt) { + evt.preventDefault() + window.propose_deal_dlog.close() +} + /* COMMON LIBRARY */ function array_insert(array, index, item) { diff --git a/rules.js b/rules.js index 9fa3eef..592d68f 100644 --- a/rules.js +++ b/rules.js @@ -81,6 +81,12 @@ const turn_name = [ "Winter 1744", ] +const DI_TURN = 0 +const DI_A_POWER = 1 +const DI_B_POWER = 2 +const DI_A_PROMISE = 3 +const DI_B_PROMISE = 4 + const F_EMPEROR_FRANCE = 1 const F_EMPEROR_AUSTRIA = 2 const F_ITALY_FRANCE = 4 @@ -1319,6 +1325,9 @@ function goto_start_turn() { else goto_politics() } + + // remove expired deals + game.deals = game.deals.filter(deal => deal[DI_TURN] < game.turn) } function goto_end_turn() { @@ -1728,11 +1737,11 @@ function draw_tc(draw, n, pow) { function give_subsidy(other) { if (other === P_BAVARIA && is_enemy_controlled_fortress(MUNCHEN)) { - log("Bavaria 1 TC lost (S" + MUNCHEN + " is enemy controlled)") + log("Bavaria TC subsidy lost\nS" + MUNCHEN + " is enemy controlled") return } if (other === P_SAXONY && is_enemy_controlled_fortress(DRESDEN)) { - log("Saxony 1 TC lost (S" + DRESDEN + " is enemy controlled)") + log("Saxony TC subsidy lost\nS" + DRESDEN + " is enemy controlled") return } draw_tc(game.hand2[other], 1, other) @@ -1752,9 +1761,9 @@ function goto_tactical_cards() { game.draw = [] if (game.power === P_BAVARIA && is_enemy_controlled_fortress(MUNCHEN)) { - log("S" + MUNCHEN + " is enemy controlled.") + log("Bavaria TC draw lost\nS" + MUNCHEN + " is enemy controlled.") } else if (game.power === P_SAXONY && is_enemy_controlled_fortress(DRESDEN)) { - log("S" + DRESDEN + " is enemy controlled.") + log("Saxony TC draw lost\nS" + DRESDEN + " is enemy controlled.") } else { let n_cards = tc_per_turn() @@ -1764,6 +1773,48 @@ function goto_tactical_cards() { if (map_get(game.contracts[game.power], other, 0) > 0) --n_cards + // Too many subsidies to hand out! + // NOTE: This can only happen with Prussia. + // If giving subsidies to Saxony, Bavaria, and/or France + // while at the -1 or -2 TC on the Russia track. + while (n_cards < 0) { + // Cancel cooperative subsidy first. + if (map_get(game.contracts[P_PRUSSIA], P_SAXONY, 0) > 0) { + log("Canceled subsidy to Saxony (forced).") + map_delete(game.contracts[P_PRUSSIA], P_SAXONY) + n_cards++ + continue + } + + // Then the shortest of the Bavarian or France subsidy. + let n_france = map_get(game.contracts[P_PRUSSIA], P_FRANCE, 0) + let n_bavaria = map_get(game.contracts[P_PRUSSIA], P_BAVARIA, 0) + if (n_france > 0 && n_bavaria > 0) { + // Cancel the shortest remaining subsidy. + if (n_france > n_bavaria) + n_france = 0 + else + n_bavaria = 0 + } + + if (n_france > 0) { + log("Canceled subsidy to France (forced).") + map_delete(game.contracts[P_PRUSSIA], P_FRANCE) + n_cards++ + continue + } + + if (n_bavaria > 0) { + log("Canceled subsidy to Bavaria (forced).") + map_delete(game.contracts[P_PRUSSIA], P_BAVARIA) + n_cards++ + continue + } + + // Should never happen! + n_cards = 0 + } + draw_tc(game.draw, n_cards, game.power) if (game.contracts[game.power]) { @@ -2094,7 +2145,7 @@ states.supply_hussars = { // put back into hand unused cards for (let c of game.supply.pool) - set_add(game.hand2[game.power], c) // TODO: or hand1 + set_add(game.hand2[game.power], c) delete game.supply.pool delete game.supply.used @@ -3215,7 +3266,7 @@ function end_re_enter_train() { // put back into hand unused cards for (let c of game.recruit.pool) - set_add(game.hand2[game.power], c) // TODO: or hand1 + set_add(game.hand2[game.power], c) delete game.recruit @@ -3531,7 +3582,7 @@ function end_recruit() { // put back into hand unused cards for (let c of game.recruit.pool) - set_add(game.hand2[game.power], c) // TODO: or hand1 + set_add(game.hand2[game.power], c) delete game.recruit } else { @@ -3972,6 +4023,7 @@ function resume_retreat() { set_active_winner() game.state = "retreat" } else { + // TODO: if mixed french/bavarian, eliminate bavarians and try again? // eliminate if there are no retreat possibilities delete game.retreat game.state = "retreat_eliminate_trapped" @@ -4760,10 +4812,10 @@ states.political_troop_power = { view.actions.pass = 1 }, power(pow) { - clear_undo() // reveal random cards let info = event_troops[current_political_effect()] set_active_to_power(pow) if (info.tcs > 0) { + clear_undo() // reveal random cards draw_tc(game.draw = [], info.tcs, game.power) game.state = "political_troops_draw" } else { @@ -4814,11 +4866,13 @@ function can_add_troops() { function goto_political_troops_place() { let info = event_troops[current_political_effect()] game.count = info.troops - log(power_name[game.power] + " " + game.count + " troops.") - if (can_add_troops()) + if (can_add_troops()) { + log(power_name[game.power] + " " + game.count + " troops.") game.state = "political_troops_place" - else + } else { + log(power_name[game.power] + " cannot receive troops.") next_execute_political_card() + } } states.political_troops_place = { @@ -5106,7 +5160,7 @@ states.recruit_for_expeditionary_corps = { prompt() { prompt("Recruit 2 troops for expeditionary corps at a price of 8 TC-points.") view.draw = game.recruit.pool - if (sum_card_values(game.recruit.pool) >= 8) + if (sum_card_values(game.recruit.pool) >= 8 || count_cards_in_hand() === 0) view.actions.next = 1 else gen_cards_in_hand() @@ -5119,7 +5173,12 @@ states.recruit_for_expeditionary_corps = { next() { push_undo() - spend_card_value(game.recruit.pool, game.recruit.used, 8) + if (sum_card_values(game.recruit.pool) >= 8) { + spend_card_value(game.recruit.pool, game.recruit.used, 8) + } else { + game.recruit.used = game.recruit.pool + game.recruit.pool = [] + } log(power_name[game.power] + " spent " + game.recruit.used.map(format_card).join(", ") + ".") @@ -5800,27 +5859,26 @@ function end_imperial_election() { /* SUBSIDY CONTRACTS - CREATE */ -function may_create_subsidy() { +function goto_propose_subsidy() { + game.proposal = { save_power: game.power, save_state: game.state, from: -1, to: -1, n: 0 } + game.state = "propose_subsidy_from" +} + +function may_propose_subsidy_from(pow) { if (is_two_player()) { - let major = coop_major_power(game.power) - if (major === P_PRAGMATIC || major === P_AUSTRIA) + if (pow === P_PRAGMATIC || pow === P_AUSTRIA) return is_saxony_austrian_ally() } return true } -function goto_propose_subsidy() { - game.proposal = { save_power: game.power, save_state: game.state, from: -1, to: -1, n: 0 } - game.state = "propose_subsidy_from" -} - states.propose_subsidy_from = { inactive: "create subsidy contract", prompt() { prompt("Subsidy contract from which major power?") - for (let from of all_major_powers) - if (is_allied_power(game.power, from)) - gen_action_power(from) + for (let pow of all_major_powers) + if (may_propose_subsidy_from(pow)) + gen_action_power(pow) }, power(from) { game.proposal.from = from @@ -5831,15 +5889,13 @@ states.propose_subsidy_from = { states.propose_subsidy_to = { inactive: "create subsidy contract", prompt() { - let player = game.power let from = game.proposal.from - prompt(`Subsidy contract between ${power_name[from]} and who?`) + prompt(`Subsidy contract from ${power_name[from]} to who?`) for (let to of (is_two_player() ? all_minor_powers : all_powers)) { if (from !== to && is_allied_power(from, to)) { if (to === P_SAXONY && is_saxony_neutral()) continue - if (is_controlled_power(player, from) || is_controlled_power(player, to)) - gen_action_power(to) + gen_action_power(to) } } }, @@ -5854,47 +5910,88 @@ states.propose_subsidy_length = { prompt() { let from = game.proposal.from let to = game.proposal.to - prompt(`Subsidy contract between ${power_name[from]} and ${power_name[to]} for how many turns?`) + prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for how many turns?`) view.actions.value = [ 1, 2, 3, 4, 5, 6 ] }, value(n) { game.proposal.n = n - if (is_controlled_power(game.power, game.proposal.from)) - set_active_to_power(game.proposal.to) - else + + if (false) { + set_active_to_power(game.proposal.from) + game.state = "propose_subsidy_approve_both" + return + } + + let ctl_from = is_controlled_power(game.power, game.proposal.from) + let ctl_to = is_controlled_power(game.power, game.proposal.to) + if (ctl_from) { + if (ctl_to) { + end_propose_subsidy(true) + } else { + set_active_to_power(game.proposal.to) + game.state = "propose_subsidy_approve_last" + } + } else { set_active_to_power(game.proposal.from) - game.state = "propose_subsidy_approve" + if (ctl_to) + game.state = "propose_subsidy_approve_last" + else + game.state = "propose_subsidy_approve_both" + } }, } -states.propose_subsidy_approve = { +states.propose_subsidy_approve_both = { inactive: "approve subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n - prompt(`Subsidy contract between ${power_name[from]} and ${power_name[to]} for ${n} turns?`) + prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) view.actions.accept = 1 view.actions.refuse = 1 }, accept() { - let from = game.proposal.from - let to = game.proposal.to - let n = game.proposal.n - log(`Subsidy contract between ${power_name[from]} and ${power_name[to]} for ${n} turns.`) - map_set(game.contracts[from], to, map_get(game.contracts[from], to, 0) + n) - end_propose_subsidy() + if (is_controlled_power(game.power, game.proposal.to)) { + end_propose_subsidy(true) + } else { + set_active_to_power(game.proposal.to) + game.state = "prpose_subsidy_approve_last" + } }, refuse() { + end_propose_subsidy(false) + }, +} + +states.propose_subsidy_approve_last = { + inactive: "approve subsidy contract", + prompt() { let from = game.proposal.from let to = game.proposal.to let n = game.proposal.n - log(`${power_name[from]} refused to create subsidy contract to ${power_name[to]} for ${n} turns.`) - end_propose_subsidy() + prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) + view.actions.accept = 1 + view.actions.refuse = 1 + }, + accept() { + end_propose_subsidy(true) + }, + refuse() { + end_propose_subsidy(false) }, } -function end_propose_subsidy() { +function end_propose_subsidy(okay) { + let from = game.proposal.from + let to = game.proposal.to + let n = game.proposal.n + if (okay) { + log(`Accepted subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns.`) + map_set(game.contracts[from], to, n) + } else { + log(`Rejected subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns.`) + } set_active_to_power(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal @@ -5903,15 +6000,13 @@ function end_propose_subsidy() { /* SUBSIDY CONTRACTS - CANCEL */ function may_cancel_subsidy() { - let player = game.power let result = false for (let from of all_major_powers) { map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return - if (is_controlled_power(player, from) || is_controlled_power(player, to)) - result = true + result = true }) } return result @@ -5925,15 +6020,13 @@ function goto_cancel_subsidy() { states.cancel_subsidy_from = { inactive: "cancel subsidy contract", prompt() { - let player = game.power prompt("Cancel which subsidy contract?") for (let from of all_major_powers) { map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return - if (is_controlled_power(player, from) || is_controlled_power(player, to)) - gen_action_power(from) + gen_action_power(from) }) } }, @@ -5946,53 +6039,146 @@ states.cancel_subsidy_from = { states.cancel_subsidy_to = { inactive: "cancel subsidy contract", prompt() { - let player = game.power let from = game.proposal.from - prompt(`Cancel subsidy contract between ${power_name[from]} and who?`) + prompt(`Cancel subsidy contract from ${power_name[from]} to who?`) map_for_each(game.contracts[from], (to, _n) => { // cannot cancel initial contract! if (from === P_FRANCE && to === P_BAVARIA && game.turn < 3) return - if (is_controlled_power(player, from) || is_controlled_power(player, to)) - gen_action_power(to) + gen_action_power(to) }) }, power(to) { - let player = game.power - let from = game.proposal.from game.proposal.to = to - if (is_controlled_power(player, from)) - set_active_to_power(to) - else - set_active_to_power(from) - game.state = "cancel_subsidy_approve" + + let ctl_from = is_controlled_power(game.power, game.proposal.from) + let ctl_to = is_controlled_power(game.power, game.proposal.to) + if (ctl_from) { + if (ctl_to) { + end_cancel_subsidy(true) + } else { + set_active_to_power(game.proposal.to) + game.state = "cancel_subsidy_approve_last" + } + } else { + set_active_to_power(game.proposal.from) + if (ctl_to) + game.state = "cancel_subsidy_approve_last" + else + game.state = "cancel_subsidy_approve_both" + } } } -states.cancel_subsidy_approve = { +states.cancel_subsidy_approve_both = { inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to - prompt(`Cancel subsidy contract between ${power_name[from]} and ${power_name[to]}?`) + prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) view.actions.accept = 1 view.actions.refuse = 1 }, accept() { + if (is_controlled_power(game.power, game.proposal.to)) { + end_cancel_subsidy(true) + } else { + set_active_to_power(game.proposal.to) + game.state = "cancel_subsidy_approve_last" + } + }, + refuse() { + end_cancel_subsidy(false) + }, +} + +states.cancel_subsidy_approve_last = { + inactive: "cancel subsidy contract", + prompt() { let from = game.proposal.from let to = game.proposal.to - log(`Canceled subsidy contract between ${power_name[from]} and ${power_name[to]}.`) + prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) + view.actions.accept = 1 + view.actions.refuse = 1 + }, + accept() { + end_cancel_subsidy(true) + }, + refuse() { + end_cancel_subsidy(false) + }, +} + +function end_cancel_subsidy(okay) { + let from = game.proposal.from + let to = game.proposal.to + if (okay) { + log(`Canceled subsidy contract from ${power_name[from]} to ${power_name[to]}.`) map_delete(game.contracts[from], to) - end_cancel_subsidy() + } else { + log(`Did not cancel subsidy contract from ${power_name[from]} to ${power_name[to]}.`) + } + set_active_to_power(game.proposal.save_power) + game.state = game.proposal.save_state + delete game.proposal +} + +/* NEGOTIATION - DEALS */ + +function goto_propose_deal(deal) { + game.proposal = { save_power: game.power, save_state: game.state, deal } + let from = game.proposal.deal[DI_A_POWER] + set_active_to_power(from) + game.state = "accept_deal_from" +} + +states.accept_deal_from = { + inactive: "accept deal", + prompt() { + let from = game.proposal.deal[DI_A_POWER] + let to = game.proposal.deal[DI_B_POWER] + prompt(`Accept ${power_name[from]} - ${power_name[to]} deal?`) + view.proposed_deal = game.proposal.deal + view.actions.accept = 1 + view.actions.refuse = 1 + }, + accept() { + let to = game.proposal.deal[DI_B_POWER] + set_active_to_power(to) + game.state = "accept_deal_to" }, refuse() { - let from = game.proposal.from - log(power_name[game.power] + ` refused to cancel subsidy contract from ${power_name[from]}.`) - end_cancel_subsidy() + end_accept_deal(false) }, } -function end_cancel_subsidy() { +states.accept_deal_to = { + inactive: "accept deal", + prompt() { + let from = game.proposal.deal[DI_A_POWER] + let to = game.proposal.deal[DI_B_POWER] + prompt(`Accept ${power_name[from]} - ${power_name[to]} deal?`) + view.proposed_deal = game.proposal.deal + view.actions.accept = 1 + view.actions.refuse = 1 + }, + accept() { + end_accept_deal(true) + }, + refuse() { + end_accept_deal(false) + }, +} + +function end_accept_deal(okay) { + let from = game.proposal.deal[DI_A_POWER] + let to = game.proposal.deal[DI_B_POWER] + if (okay) { + log(`Accepted ${power_name[from]} - ${power_name[to]} deal.`) + game.deals.push(game.proposal.deal) + } else { + log(`Rejected ${power_name[from]} - ${power_name[to]} deal.`) + } set_active_to_power(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal @@ -6185,6 +6371,7 @@ exports.setup = function (seed, scenario, _options) { hand1: [ [], [], [], [], [], [] ], hand2: [ [], [], [], [], [], [] ], + deals: [], // [ power, promise, turn ] tuples contracts: [ [ P_BAVARIA, 3 ], [], @@ -6453,6 +6640,7 @@ exports.view = function (state, player) { pt: total_troops_list(), discard: total_discard_list(), + deals: game.deals, contracts: game.contracts, face_up: game.face_up, face_down: mask_face_down(), @@ -6497,11 +6685,13 @@ exports.view = function (state, player) { } // subsidy contracts actions for active player - if (!game.proposal && !is_intro()) { + if (!game.proposal && !is_intro() && view.turn > 0) { if (may_cancel_subsidy()) view.actions.cancel_subsidy = 1 - if (may_create_subsidy()) + if (!is_intro()) view.actions.propose_subsidy = 1 + if (!is_two_player() && !is_intro()) + view.actions.propose_deal = 1 } } @@ -6523,6 +6713,9 @@ exports.action = function (state, _player, action, arg) { } else if (action === "cancel_subsidy") { push_undo() goto_cancel_subsidy() + } else if (action === "propose_deal") { + push_undo() + goto_propose_deal(arg) } else throw new Error("Invalid action: " + action) } @@ -6606,6 +6799,8 @@ function push_undo() { let copy = {} for (let k in game) { let v = game[k] + if (k === "deals") + continue if (k === "undo") continue else if (k === "log") @@ -6622,10 +6817,12 @@ function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo + let save_deals = game.deals game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo + game.deals = save_deals } } -- cgit v1.2.3