diff options
author | Tor Andersson <tor@ccxvii.net> | 2024-11-24 15:53:08 +0100 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2024-11-24 15:53:08 +0100 |
commit | a2ea835f0df0f1bcd5d50792679626f85f39cc75 (patch) | |
tree | 00348fc4aad99451539d515f3caffa56f1636799 | |
parent | 090b2edecbc7bb5dad76d5ed50570f6b413ff462 (diff) | |
download | maria-a2ea835f0df0f1bcd5d50792679626f85f39cc75.tar.gz |
Validate promises.
Rework movement restarts.
Use "Reject" instead of "Refuse".
Use "Resume" after jumping back in time.
# Conflicts:
# rules.js
-rw-r--r-- | play.css | 15 | ||||
-rw-r--r-- | play.html | 15 | ||||
-rw-r--r-- | play.js | 74 | ||||
-rw-r--r-- | rules.js | 659 |
4 files changed, 561 insertions, 202 deletions
@@ -227,6 +227,7 @@ dialog button { #political_body td { border: 1px solid #0004; padding: 4px; + vertical-align: top; } #political_body td img { @@ -354,20 +355,6 @@ dialog button { #log div.i { padding-left: 32px; text-indent: -12px; } #log div.ii { padding-left: 44px; text-indent: -12px; } -#log div.deal { - font-style: italic; - border: 1px solid #0008; - background-color: #ede5a7; - padding: 4px 8px; - text-indent: 0; - margin: 0 8px; -} - -#log div.deal.rejected { - color: #0008; - text-decoration: line-through; -} - #log span.suit { font-size: 11px; } span.value { padding: 0 1px } @@ -82,6 +82,7 @@ <div id="pc_show" style="display:flex;flex-wrap:wrap;gap:18px;"> </div> </div> + <div class="deal" id="subsidy_list"></div> <div class="deal" id="active_deal_list"></div> <div class="deal" id="proposed_deal_list"></div> </div> @@ -149,7 +150,7 @@ <dialog id="propose_deal_dlog"> <form id="propose_deal_form"> - <select name="a_power"> + <select name="a_power" onchange="update_deal_options()"> <option value="0">France</option> <option value="1">Prussia</option> <option value="2">Pragmatic Army</option> @@ -158,10 +159,15 @@ <option value="5">Saxony</option> </select> <br> + <label><input type="checkbox" name="a_phase_politics">Politics</label> + <label><input type="checkbox" name="a_phase_movement">Movement</label> + <label><input type="checkbox" name="a_phase_retreat">Retreat</label> + <label id="a_hussars_label"><input type="checkbox" name="a_phase_hussars">Hussars</label> + <br> <textarea name="a_promise" placeholder="Promise..." rows=3 cols=40></textarea> <br><br> - <select name="b_power"> + <select name="b_power" onchange="update_deal_options()"> <option value="0">France</option> <option value="1">Prussia</option> <option value="2">Pragmatic Army</option> @@ -170,6 +176,11 @@ <option value="5">Saxony</option> </select> <br> + <label><input type="checkbox" name="b_phase_politics">Politics</label> + <label><input type="checkbox" name="b_phase_movement">Movement</label> + <label><input type="checkbox" name="b_phase_retreat">Retreat</label> + <label id="b_hussars_label"><input type="checkbox" name="b_phase_hussars">Hussars</label> + <br> <textarea name="b_promise" placeholder="Promise..." rows=3 cols=40></textarea> <br><br> @@ -118,6 +118,12 @@ const F_TWO_PLAYER = 8192 const F_PLAYER_A_PICK = 16384 // picked PC card const F_PLAYER_B_PICK = 32768 +// promise validation phases +const V_POLITICS = 1 +const V_HUSSARS = 2 +const V_MOVEMENT = 4 +const V_RETREAT = 8 + const SPADES = 0 const CLUBS = 1 const HEARTS = 2 @@ -1574,6 +1580,7 @@ function on_update() { "ping", ]) + update_subsidy_list(view.contracts, window.subsidy_list, "Active Subsidies") 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") @@ -1620,7 +1627,9 @@ function on_update() { action_button("reduce", "Reduce") action_button("peace", "Peace") action_button("accept", "Accept") - action_button("refuse", "Refuse") + action_button("reject", "Reject") + action_button("yes", "Yes") + action_button("no", "No") action_button("re_enter", "Re-enter") action_button("force_march", "Force march") @@ -1766,20 +1775,51 @@ function html_escape(str) { return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") } +function phase_list(phase) { + let list = [] + if (phase & V_POLITICS) list.push("politics") + if (phase & V_HUSSARS) list.push("hussars") + if (phase & V_MOVEMENT) list.push("movement") + if (phase & V_RETREAT) list.push("retreat") + return list.join(" ") +} + +function update_subsidy_list(subs, elt, title) { + let show = false + let str = "<table><tr><th colspan=3>" + title + for (let from of all_major_powers) { + map_for_each(subs[from], (to, n) => { + show = true + str += "<tr><td width=24>" + power_image[from] + str += "<td width=24>" + power_image[to] + str += "<td>" + n + " TC" + }) + } + if (show) { + elt.innerHTML = str + elt.style.display = "block" + } else { + elt.style.display = "none" + } +} + function update_deal_item(deal) { - let [ turn, a_power, b_power, a_promise, b_promise ] = deal + let [ turn, a_power, b_power, a_promise, b_promise, a_phase, b_phase ] = deal let str = "<tr>" str += "<td width=24>" + power_image[a_power] str += "<td>" + html_escape(a_promise) - str += "<td width=24>" + power_image[b_power] - str += "<td>" + html_escape(b_promise) + str += "<td width=80>" + phase_list(a_phase) str += "<td width=60>" + turn_name[turn] + str += "<tr>" + str += "<td>" + power_image[b_power] + str += "<td>" + html_escape(b_promise) + str += "<td>" + phase_list(b_phase) return str } function update_deal_list(deals, elt, title) { if (deals && deals.length > 0) { - let str = "<table><tr><th colspan=5>" + title + let str = "<table><tr><th colspan=4>" + title for (let deal of deals) str += update_deal_item(deal) str += "</table>" @@ -1790,6 +1830,13 @@ function update_deal_list(deals, elt, title) { } } +function update_deal_options() { + let a_power = window.propose_deal_form.elements.a_power.value | 0 + let b_power = window.propose_deal_form.elements.b_power.value | 0 + window.a_hussars_label.classList.toggle("hide", a_power !== P_AUSTRIA) + window.b_hussars_label.classList.toggle("hide", b_power !== P_AUSTRIA) +} + function propose_deal() { if (!is_action("propose_deal")) return @@ -1816,6 +1863,8 @@ function propose_deal() { for (let opt of form.turn.options) opt.disabled = (opt.value < view.turn) + update_deal_options() + window.propose_deal_dlog.showModal() } @@ -1828,6 +1877,19 @@ function propose_deal_submit(evt) { data.a_power = Number(data.a_power) data.b_power = Number(data.b_power) + data.a_phase = 0 + data.b_phase = 0 + data.a_phase |= (data.a_phase_politics ? V_POLITICS : 0) + data.b_phase |= (data.b_phase_politics ? V_POLITICS : 0) + if (data.a_power === P_AUSTRIA) + data.a_phase |= (data.a_phase_hussars ? V_HUSSARS : 0) + if (data.b_power === P_AUSTRIA) + data.b_phase |= (data.b_phase_hussars ? V_HUSSARS : 0) + data.a_phase |= (data.a_phase_movement ? V_MOVEMENT : 0) + data.b_phase |= (data.b_phase_movement ? V_MOVEMENT : 0) + data.a_phase |= (data.a_phase_retreat ? V_RETREAT : 0) + data.b_phase |= (data.b_phase_retreat ? V_RETREAT : 0) + if (!data.a_promise || !data.b_promise) { alert("Each side must promise something!") return @@ -1839,7 +1901,7 @@ function propose_deal_submit(evt) { return } - let deal = [ data.turn, data.a_power, data.b_power, data.a_promise, data.b_promise ] + let deal = [ data.turn, data.a_power, data.b_power, data.a_promise, data.b_promise, data.a_phase, data.b_phase ] if (view.actions.propose_deal) send_message("action", [ "propose_deal", deal, game_cookie ]) @@ -88,6 +88,8 @@ const DI_A_POWER = 1 const DI_B_POWER = 2 const DI_A_PROMISE = 3 const DI_B_PROMISE = 4 +const DI_A_PHASE = 5 +const DI_B_PHASE = 6 const F_EMPEROR_FRANCE = 1 const F_EMPEROR_AUSTRIA = 2 @@ -107,6 +109,12 @@ const F_TWO_PLAYER = 8192 const F_PLAYER_A_PICK = 16384 // picked PC card const F_PLAYER_B_PICK = 32768 +// promise validation phases +const V_POLITICS = 1 +const V_HUSSARS = 2 +const V_MOVEMENT = 4 +const V_RETREAT = 8 + const SPADES = 0 const CLUBS = 1 const HEARTS = 2 @@ -1416,9 +1424,8 @@ const advanced_sequence_of_play = [ { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, - { power: P_FRANCE, action: init_movement }, - { power: P_FRANCE, action: goto_movement_global }, - { power: P_FRANCE, action: end_movement }, + { power: P_FRANCE, action: goto_movement }, + { power: P_FRANCE, action: validate_end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, { power: P_FRANCE, action: goto_france_reduces_military_objectives }, @@ -1429,9 +1436,8 @@ const advanced_sequence_of_play = [ { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_prussian }, - { power: P_PRUSSIA, action: init_movement }, - { power: P_PRUSSIA, action: goto_movement_global }, - { power: P_PRUSSIA, action: end_movement }, + { power: P_PRUSSIA, action: goto_movement }, + { power: P_PRUSSIA, action: validate_end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, { power: P_PRUSSIA, action: goto_prussia_annexes_silesia }, @@ -1446,9 +1452,8 @@ const advanced_sequence_of_play = [ { power: P_PRAGMATIC, action: goto_supply }, // alternate moves on flanders starting with pragmatic army - { power: P_PRAGMATIC, action: init_movement }, - { power: P_PRAGMATIC, action: goto_movement_flanders }, - { power: P_PRAGMATIC, action: end_movement }, + { power: P_PRAGMATIC, action: goto_movement }, + { power: P_PRAGMATIC, action: validate_end_movement }, { power: P_AUSTRIA, action: goto_combat }, { power: P_PRAGMATIC, action: goto_combat }, @@ -1465,8 +1470,7 @@ const two_player_sequence_of_play = [ { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, - { power: P_FRANCE, action: init_movement }, - { power: P_FRANCE, action: goto_movement_global }, + { power: P_FRANCE, action: goto_movement }, { power: P_FRANCE, action: end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, @@ -1478,8 +1482,7 @@ const two_player_sequence_of_play = [ { power: P_SAXONY, action: goto_tactical_cards, condition: is_saxony_prussian }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply, condition: is_saxony_prussian }, - { power: P_PRUSSIA, action: init_movement }, - { power: P_PRUSSIA, action: goto_movement_global }, + { power: P_PRUSSIA, action: goto_movement }, { power: P_PRUSSIA, action: end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, @@ -1494,8 +1497,7 @@ const two_player_sequence_of_play = [ { power: P_PRAGMATIC, action: goto_supply }, // austria and pragmatic share movement (like with minor power) - { power: P_AUSTRIA, action: init_movement }, - { power: P_AUSTRIA, action: goto_movement_global }, + { power: P_AUSTRIA, action: goto_movement }, { power: P_AUSTRIA, action: end_movement }, // austria and pragmatic share combat (like with minor power) @@ -1514,8 +1516,7 @@ const intro_sequence_of_play = [ { power: P_BAVARIA, action: goto_tactical_cards }, { power: P_FRANCE, action: goto_supply }, { power: P_BAVARIA, action: goto_supply }, - { power: P_FRANCE, action: init_movement }, - { power: P_FRANCE, action: goto_movement_global }, + { power: P_FRANCE, action: goto_movement }, { power: P_FRANCE, action: end_movement }, { power: P_FRANCE, action: goto_combat }, { power: P_FRANCE, action: end_action_stage }, @@ -1525,8 +1526,7 @@ const intro_sequence_of_play = [ { power: P_SAXONY, action: goto_tactical_cards }, { power: P_PRUSSIA, action: goto_supply }, { power: P_SAXONY, action: goto_supply }, - { power: P_PRUSSIA, action: init_movement }, - { power: P_PRUSSIA, action: goto_movement_global }, + { power: P_PRUSSIA, action: goto_movement }, { power: P_PRUSSIA, action: end_movement }, { power: P_PRUSSIA, action: goto_combat }, { power: P_PRUSSIA, action: end_action_stage }, @@ -1534,8 +1534,7 @@ const intro_sequence_of_play = [ { power: P_AUSTRIA, action: start_action_stage }, { power: P_AUSTRIA, action: goto_tactical_cards }, { power: P_AUSTRIA, action: goto_supply }, - { power: P_AUSTRIA, action: init_movement }, - { power: P_AUSTRIA, action: goto_movement_global }, + { power: P_AUSTRIA, action: goto_movement }, { power: P_AUSTRIA, action: end_movement }, { power: P_AUSTRIA, action: goto_combat }, { power: P_AUSTRIA, action: end_action_stage }, @@ -1602,9 +1601,13 @@ function goto_place_hussars() { log("=" + P_AUSTRIA + " Hussars") set_active_to_power(P_AUSTRIA) game.state = "place_hussars" + set_clear(game.moved) + save_checkpoint() } function end_place_hussars() { + clear_checkpoint() + set_clear(game.moved) for (let p of all_hussars) log("P" + p + " at S" + game.pos[p] + ".") @@ -1629,7 +1632,9 @@ states.place_hussars = { game.state = "place_hussars_where" }, end_place_hussars() { - end_place_hussars() + game.proposal = game.undo[0] + clear_undo() + goto_validate_hussars() }, } @@ -2360,7 +2365,7 @@ function give_troops(total) { /* MOVEMENT */ -function init_movement() { +function goto_movement() { set_clear(game.moved) if (is_intro()) { @@ -2374,7 +2379,8 @@ function init_movement() { game.move_re_entered = 0 game.move_conq = [] - next_sequence_of_play() + game.state = "movement" + save_checkpoint() } function has_unmoved_piece(pow) { @@ -2398,20 +2404,6 @@ function has_unmoved_piece_on_flanders_map(pow) { return false } -function goto_movement_flanders() { - game.state = "movement" - - // save RESTART point - clear_undo() - push_undo() - game.restart = game.undo[0] - clear_undo() -} - -function goto_movement_global() { - game.state = "movement" -} - function resume_movement() { if (is_two_player() && game.power === P_PRAGMATIC) game.power = P_AUSTRIA @@ -2511,6 +2503,12 @@ states.movement_flanders_next = { states.movement = { inactive: "move", + dont_snap() { + // don't snapshot during alternating move phase + if (!is_two_player() && !is_intro() && (game.power === P_PRAGMATIC || game.power === P_AUSTRIA)) + return true + return false + }, prompt() { let done_generals = true let done_trains = true @@ -2631,50 +2629,49 @@ states.movement = { }, restart() { let dispute = game.flags & F_MOVE_DISPUTE - game.undo = [ game.restart ] - delete game.restart - pop_undo() - goto_movement_flanders() + restore_checkpoint() game.state = "restart_flanders_movement" game.flags |= dispute // preserve disputed mode }, dispute() { - game.undo = [ game.restart ] - delete game.restart - pop_undo() - goto_movement_flanders() + restore_checkpoint() game.state = "dispute_flanders_movement" game.flags |= F_MOVE_DISPUTE }, } states.restart_flanders_movement = { + dont_snap: true, inactive: "move", prompt() { prompt("Reset moves on Flanders map due to request.") - view.actions.next = 1 + view.actions.resume = 1 + view.actions.undo = 0 }, - next() { + resume() { game.state = "movement" }, } states.dispute_flanders_movement = { + dont_snap: true, inactive: "move", prompt() { prompt("Alternating moves on Flanders map due to dispute.") - view.actions.next = 1 + view.actions.resume = 1 + view.actions.undo = 0 }, - next() { + resume() { log("Disputed Flanders movement.") game.state = "movement" }, } function end_movement() { + clear_checkpoint() + game.flags &= ~F_MOVE_DISPUTE game.flags &= ~F_MOVE_FLANDERS - delete game.restart if (game.moved.length === 0) log("Nothing moved.") @@ -3151,6 +3148,7 @@ function goto_confirm_flanders_stack() { } states.confirm_flanders_stack_1 = { + dont_snap: true, inactive: "move", prompt() { let p = game.selected @@ -3179,6 +3177,7 @@ states.confirm_flanders_stack_1 = { } states.confirm_flanders_stack_2 = { + dont_snap: true, inactive: "confirm mixed stack", prompt() { let p = game.selected @@ -3191,20 +3190,21 @@ states.confirm_flanders_stack_2 = { other = find_general_of_power(s, P_PRAGMATIC) prompt(`${piece_name[p]} wants to stack with ${piece_name[other]} at ${data.cities.name[s]}.`) view.actions.accept = 1 - view.actions.refuse = 1 + view.actions.reject = 1 }, accept() { push_undo() delete game.proposal resume_movement_after_flanders_stacking() }, - refuse() { + reject() { set_active_to_power(piece_power[game.selected]) game.state = "confirm_flanders_stack_3" }, } states.confirm_flanders_stack_3 = { + dont_snap: true, inactive: "move", prompt() { let p = game.selected @@ -4286,7 +4286,7 @@ states.retreat_done = { view.actions.next = 1 }, next() { - finish_combat() + goto_validate_retreat() }, } @@ -4305,6 +4305,8 @@ function lose_vp(pow, n) { } function finish_combat() { + game.selected = -1 + if (is_intro()) { delete game.lost_generals delete game.lost_troops @@ -5728,13 +5730,13 @@ states.accept_peace = { prompt() { prompt("Accept Prussia's offer of peace for annexion of Silesia?") view.actions.accept = 1 - view.actions.refuse = 1 + view.actions.reject = 1 }, accept() { log("Austria accepts peace.") goto_annex_silesia() }, - refuse() { + reject() { log("Austria refuses peace.") next_sequence_of_play() }, @@ -6080,9 +6082,9 @@ function end_imperial_election() { /* SUBSIDY CONTRACTS - CREATE */ -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 undo_subsidy_wizard() { + game.state = game.proposal.save_state + delete game.proposal } function may_propose_subsidy_from(pow) { @@ -6093,21 +6095,39 @@ function may_propose_subsidy_from(pow) { return true } +function create_subsidy() { + let from = game.proposal.from + let to = game.proposal.to + let n = game.proposal.n + map_set(game.contracts[from], to, n) +} + +function goto_propose_subsidy(save_power) { + game.proposal = { save_power, save_state: game.state, from: -1, to: -1, n: 0 } + game.state = "propose_subsidy_from" +} + states.propose_subsidy_from = { + dont_snap: true, inactive: "create subsidy contract", prompt() { prompt("Subsidy contract from which major power?") for (let pow of all_major_powers) if (may_propose_subsidy_from(pow)) gen_action_power(pow) + view.actions.undo = 1 }, power(from) { game.proposal.from = from game.state = "propose_subsidy_to" - } + }, + undo() { + undo_subsidy_wizard() + }, } states.propose_subsidy_to = { + dont_snap: true, inactive: "create subsidy contract", prompt() { let from = game.proposal.from @@ -6121,50 +6141,46 @@ states.propose_subsidy_to = { gen_action_power(to) } } + view.actions.undo = 1 }, power(to) { game.proposal.to = to game.state = "propose_subsidy_length" - } + }, + undo() { + undo_subsidy_wizard() + }, } states.propose_subsidy_length = { + dont_snap: true, inactive: "create subsidy contract", prompt() { let from = game.proposal.from let to = game.proposal.to prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for how many turns?`) view.actions.value = [ 1, 2, 3, 4, 5, 6 ] + view.actions.undo = 1 }, value(n) { game.proposal.n = n - - 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" - } + if (ctl_from && ctl_to) { + create_subsidy() + end_propose_subsidy() } else { - set_active_to_power(game.proposal.from) - if (ctl_to) - game.state = "propose_subsidy_approve_last" - else - game.state = "propose_subsidy_approve_both" + set_active_to_power_keep_undo(game.proposal.from) + game.state = "propose_subsidy_approve_from" } }, + undo() { + undo_subsidy_wizard() + }, } -states.propose_subsidy_approve_both = { +states.propose_subsidy_approve_from = { + dont_snap: true, inactive: "approve subsidy contract", prompt() { let from = game.proposal.from @@ -6172,22 +6188,19 @@ states.propose_subsidy_approve_both = { let n = game.proposal.n prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) view.actions.accept = 1 - view.actions.refuse = 1 + view.actions.reject = 1 }, accept() { - if (is_controlled_power(game.power, game.proposal.to)) { - end_propose_subsidy(true) - } else { - set_active_to_power(game.proposal.to) - game.state = "propose_subsidy_approve_last" - } + set_active_to_power_keep_undo(game.proposal.to) + game.state = "propose_subsidy_approve_to" }, - refuse() { - end_propose_subsidy(false) + reject() { + goto_reject_propose_subsidy() }, } -states.propose_subsidy_approve_last = { +states.propose_subsidy_approve_to = { + dont_snap: true, inactive: "approve subsidy contract", prompt() { let from = game.proposal.from @@ -6195,27 +6208,50 @@ states.propose_subsidy_approve_last = { let n = game.proposal.n prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns?`) view.actions.accept = 1 - view.actions.refuse = 1 + view.actions.reject = 1 }, accept() { - end_propose_subsidy(true) + create_subsidy() + end_propose_subsidy() }, - refuse() { - end_propose_subsidy(false) + reject() { + goto_reject_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) +function goto_reject_propose_subsidy() { + if (game.power === game.proposal.to) + set_active_to_power_keep_undo(game.proposal.from) + else + set_active_to_power_keep_undo(game.proposal.save_power) + game.state = "reject_propose_subsidy" +} + +function next_reject_propose_subsidy() { + if (game.power !== game.proposal.save_power) + set_active_to_power_keep_undo(game.proposal.save_power) + else + end_propose_subsidy() +} + +states.reject_propose_subsidy = { + dont_snap: true, + inactive: "approve subsidy contract", + prompt() { + let from = game.proposal.from + let to = game.proposal.to + let n = game.proposal.n + prompt(`Subsidy contract from ${power_name[from]} to ${power_name[to]} for ${n} turns was NOT accepted.`) + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + next_reject_propose_subsidy() + }, +} + +function end_propose_subsidy() { + set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } @@ -6235,12 +6271,19 @@ function may_cancel_subsidy() { return result } -function goto_cancel_subsidy() { - game.proposal = { save_power: game.power, save_state: game.state, from: -1, to: -1 } +function cancel_subsidy() { + let from = game.proposal.from + let to = game.proposal.to + map_delete(game.contracts[from], to) +} + +function goto_cancel_subsidy(save_power) { + game.proposal = { save_power, save_state: game.state, from: -1, to: -1 } game.state = "cancel_subsidy_from" } states.cancel_subsidy_from = { + dont_snap: true, inactive: "cancel subsidy contract", prompt() { prompt("Cancel which subsidy contract?") @@ -6252,14 +6295,19 @@ states.cancel_subsidy_from = { gen_action_power(from) }) } + view.actions.undo = 1 }, power(from) { game.proposal.from = from game.state = "cancel_subsidy_to" - } + }, + undo() { + undo_subsidy_wizard() + }, } states.cancel_subsidy_to = { + dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from @@ -6270,30 +6318,28 @@ states.cancel_subsidy_to = { return gen_action_power(to) }) + view.actions.undo = 1 }, power(to) { game.proposal.to = to 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" - } + if (ctl_from && ctl_to) { + cancel_subsidy() + end_cancel_subsidy() } else { - set_active_to_power(game.proposal.from) - if (ctl_to) - game.state = "cancel_subsidy_approve_last" - else - game.state = "cancel_subsidy_approve_both" + set_active_to_power_keep_undo(game.proposal.from) + game.state = "cancel_subsidy_approve_from" } - } + }, + undo() { + undo_subsidy_wizard() + }, } -states.cancel_subsidy_approve_both = { +states.cancel_subsidy_approve_from = { + dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from @@ -6301,22 +6347,19 @@ states.cancel_subsidy_approve_both = { prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) view.actions.accept = 1 // TODO: cannot refuse if minor power fortress is occupied! - view.actions.refuse = 1 + view.actions.reject = 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" - } + set_active_to_power_keep_undo(game.proposal.to) + game.state = "cancel_subsidy_approve_to" }, - refuse() { - end_cancel_subsidy(false) + reject() { + goto_reject_cancel_subsidy() }, } -states.cancel_subsidy_approve_last = { +states.cancel_subsidy_approve_to = { + dont_snap: true, inactive: "cancel subsidy contract", prompt() { let from = game.proposal.from @@ -6324,26 +6367,49 @@ states.cancel_subsidy_approve_last = { prompt(`Cancel subsidy contract from ${power_name[from]} to ${power_name[to]}?`) view.actions.accept = 1 // TODO: cannot refuse if minor power fortress is occupied! - view.actions.refuse = 1 + view.actions.reject = 1 }, accept() { - end_cancel_subsidy(true) + cancel_subsidy() + end_cancel_subsidy() }, - refuse() { - end_cancel_subsidy(false) + reject() { + goto_reject_cancel_subsidy() }, } -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) - } else { - log(`Did not cancel subsidy contract from ${power_name[from]} to ${power_name[to]}.`) - } - set_active_to_power(game.proposal.save_power) +function goto_reject_cancel_subsidy() { + if (game.power === game.proposal.to) + set_active_to_power_keep_undo(game.proposal.from) + else + set_active_to_power_keep_undo(game.proposal.save_power) + game.state = "reject_cancel_subsidy" +} + +function next_reject_cancel_subsidy() { + if (game.power !== game.proposal.save_power) + set_active_to_power_keep_undo(game.proposal.save_power) + else + end_cancel_subsidy() +} + +states.reject_cancel_subsidy = { + dont_snap: true, + inactive: "cancel subsidy contract", + prompt() { + let from = game.proposal.from + let to = game.proposal.to + prompt(`Subsidy from ${power_name[from]} to ${power_name[to]} was NOT canceled.`) + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + next_reject_cancel_subsidy() + }, +} + +function end_cancel_subsidy() { + set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } @@ -6357,24 +6423,16 @@ function goto_propose_deal(save_power, deal) { game.state = "accept_deal_from" } -function log_deal(deal, prefix, postfix = "") { - log(prefix + - "Deal until " + turn_name[deal[DI_TURN]] + ".\n" + - power_name[deal[DI_A_POWER]] + ": " + deal[DI_A_PROMISE] + "\n" + - power_name[deal[DI_B_POWER]] + ": " + deal[DI_B_PROMISE] + "\n" + - postfix - ) -} - states.accept_deal_from = { - inactive: "accept deal", + dont_snap: true, + inactive: "negotiate", 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 + view.actions.reject = 1 view.actions.undo = 0 }, accept() { @@ -6382,55 +6440,85 @@ states.accept_deal_from = { set_active_to_power_keep_undo(to) game.state = "accept_deal_to" }, - refuse() { - end_accept_deal(false) + reject() { + goto_reject_deal() }, } states.accept_deal_to = { - inactive: "accept deal", + dont_snap: true, + inactive: "negotiate", 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 + view.actions.reject = 1 view.actions.undo = 0 }, accept() { - end_accept_deal(true) + game.deals.push(game.proposal.deal) + end_accept_deal() }, - refuse() { - end_accept_deal(false) + reject() { + goto_reject_deal() }, } -function end_accept_deal(okay) { - if (okay) { - log_deal(game.proposal.deal, "{") - game.deals.push(game.proposal.deal) - } else { - log_deal(game.proposal.deal, "}", "Rejected by " + power_name[game.power] + ".") - } - log_br() +function goto_reject_deal() { + let from = game.proposal.deal[DI_A_POWER] + let to = game.proposal.deal[DI_B_POWER] + if (game.power === to) + set_active_to_power_keep_undo(from) + else + set_active_to_power_keep_undo(game.proposal.save_power) + game.state = "reject_deal" +} + +function next_reject_deal() { + if (game.power !== game.proposal.save_power) + set_active_to_power_keep_undo(game.proposal.save_power) + else + end_accept_deal() +} +states.reject_deal = { + dont_snap: true, + inactive: "negotiate", + prompt() { + let from = game.proposal.deal[DI_A_POWER] + let to = game.proposal.deal[DI_B_POWER] + prompt(`Deal between ${power_name[from]} and ${power_name[to]} was NOT accepted.`) + view.proposed_deal = game.proposal.deal + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + next_reject_cancel_subsidy() + }, +} + +function end_accept_deal() { set_active_to_power_keep_undo(game.proposal.save_power) game.state = game.proposal.save_state delete game.proposal } +/* NEGOTIATION - PING PLAYER */ + function goto_ping() { game.proposal = { save_power: game.power, save_state: game.state } game.state = "ping" } states.ping = { + dont_snap: true, inactive: "ping", prompt() { prompt("Ping which power to respond to chat?") for (let pow of all_powers) - if (pow !== game.power) + if (!is_controlled_power(game.power, pow)) gen_action_power(pow) view.actions.undo = 1 }, @@ -6444,6 +6532,7 @@ states.ping = { } states.pong = { + dont_snap: true, inactive: "respond", prompt() { prompt(power_name[game.proposal.save_power] + " has requested your response in chat.") @@ -6468,6 +6557,187 @@ states.pong = { }, } +/* VALIDATE PROMISES */ + +function should_validate_promise(me, you, phase) { + for (let deal of game.deals) { + if (deal[DI_A_POWER] === me && deal[DI_B_POWER] === you && (deal[DI_A_PHASE] & phase)) + return true + if (deal[DI_B_POWER] === me && deal[DI_A_POWER] === you && (deal[DI_B_PHASE] & phase)) + return true + } + return false +} + +function goto_validate_promise(phase, resume) { + game.proposal = 0 + for (let other of all_powers) + if (should_validate_promise(game.power, other, phase)) + game.proposal |= (1 << other) + resume() +} + +function resume_validate_promise(state, end) { + if (game.proposal) { + for (let other of all_powers) { + if (game.proposal & (1 << other)) { + game.proposal &= ~(1 << other) + game.state = state + set_active_to_power(other) + return + } + } + } + delete game.proposal + end() +} + +// TODO: validate_political_card + +function goto_validate_hussars() { + game.proposal = 0 + for (let other of all_powers) + if (should_validate_promise(game.power, other, V_HUSSARS)) + game.proposal |= (1 << other) + resume_validate_hussars() +} + +function resume_validate_hussars() { + resume_validate_promise("validate_hussars", end_place_hussars) +} + +states.validate_hussars = { + dont_snap: true, + inactive: "validate promise", + prompt() { + let other = current_sequence_of_play().power + prompt("Did " + power_name[other] + " keep their hussar promise?") + view.actions.yes = 1 + view.actions.no = 1 + }, + yes() { + resume_validate_hussars() + }, + no() { + let other = game.power + restore_checkpoint() + game.proposal = other + game.state = "validate_hussars_fail" + }, +} + +states.validate_hussars_fail = { + dont_snap: true, + inactive: "place hussars", + prompt() { + prompt("Promise to " + power_name[game.proposal] + " was not kept.") + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + game.state = "place_hussars" + delete game.proposal + }, +} + +function should_validate_movement(power) { + for (let other of all_powers) + if (should_validate_promise(power, other, V_MOVEMENT)) + game.proposal |= (1 << other) +} + +function validate_end_movement() { + game.proposal = 0 + should_validate_movement(game.power) + if (game.power === P_PRAGMATIC) + should_validate_movement(P_AUSTRIA) + resume_validate_movement() +} + +function resume_validate_movement() { + resume_validate_promise("validate_movement", end_movement) +} + +states.validate_movement = { + dont_snap: true, + inactive: "validate promise", + prompt() { + let other = current_sequence_of_play().power + prompt("Did " + power_name[other] + " keep their movement promise?") + view.actions.yes = 1 + view.actions.no = 1 + }, + yes() { + resume_validate_movement() + }, + no() { + let other = game.power + restore_checkpoint() + game.proposal = other + game.state = "validate_movement_fail" + }, +} + +states.validate_movement_fail = { + dont_snap: true, + inactive: "move", + prompt() { + prompt("Promise to " + power_name[game.proposal] + " was not kept. Movement phase restarted.") + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + game.state = "movement" + delete game.proposal + }, +} + +function goto_validate_retreat() { + // TODO: all powers + let other = piece_power[game.selected[0]] + if (should_validate_promise(game.power, other, V_RETREAT)) { + set_active_to_power_keep_undo(other) + game.state = "validate_retreat" + } else { + finish_combat() + } +} + +states.validate_retreat = { + dont_snap: true, + inactive: "validate promise", + prompt() { + let other = current_sequence_of_play().power + prompt("Did " + power_name[other] + " keep their retreat promise?") + view.actions.yes = 1 + view.actions.no = 1 + }, + yes() { + clear_undo() + finish_combat() + }, + no() { + let other = game.power + pop_undo() + game.proposal = other + game.state = "validate_retreat_fail" + }, +} + +states.validate_retreat_fail = { + dont_snap: true, + inactive: "retreat defeated general", + prompt() { + prompt("Promise to " + power_name[game.proposal] + " was not kept.") + view.actions.resume = 1 + view.actions.undo = 0 + }, + resume() { + game.state = "retreat" + delete game.proposal + }, +} + /* SETUP */ const POWER_FROM_SETUP_STAGE = [ @@ -6974,8 +7244,8 @@ exports.view = function (state, player) { view.actions.undo = 0 } - // subsidy contracts actions for active player - if (!game.proposal && !is_intro() && view.turn > 0) { + // negotiation actions for active player + if (game.proposal === undefined && !is_intro() && view.turn > 0) { if (may_cancel_subsidy()) view.actions.cancel_subsidy = 1 if (!is_intro()) @@ -6998,11 +7268,9 @@ exports.action = function (state, _player, action, arg) { if (action === "undo" && game.undo && game.undo.length > 0) { pop_undo() } else if (action === "propose_subsidy") { - push_undo() - goto_propose_subsidy() + goto_propose_subsidy(game.power) } else if (action === "cancel_subsidy") { - push_undo() - goto_cancel_subsidy() + goto_cancel_subsidy(game.power) } else if (action === "propose_deal") { goto_propose_deal(game.power, arg) } else if (action === "ping") { @@ -7013,6 +7281,15 @@ exports.action = function (state, _player, action, arg) { return game } +exports.dont_snap = function (state) { + let dont_snap = states[state.state].dont_snap + if (typeof dont_snap === "function") { + game = state + return dont_snap() + } + return !!dont_snap +} + /* COMMON FRAMEWORK */ function goto_game_over(result, victory_reason) { @@ -7081,6 +7358,24 @@ function log_br() { /* COMMON LIBRARY */ +function save_checkpoint() { + clear_undo() + push_undo() + game.restart = game.undo.pop() +} + +function restore_checkpoint() { + game.undo = [ game.restart ] + delete game.restart + pop_undo() + push_undo() + game.restart = game.undo.pop() +} + +function clear_checkpoint() { + delete game.restart +} + function clear_undo() { game.undo = [] } @@ -7090,6 +7385,8 @@ function push_undo() { let copy = {} for (let k in game) { let v = game[k] + if (k === "subsidies") + continue if (k === "deals") continue if (k === "restart") @@ -7110,12 +7407,14 @@ function pop_undo() { if (game.undo) { let save_log = game.log let save_undo = game.undo + let save_subsidies = game.subsidies let save_deals = game.deals let save_restart = game.restart game = save_undo.pop() save_log.length = game.log game.log = save_log game.undo = save_undo + game.subsidies = save_subsidies game.deals = save_deals game.restart = save_restart } |