Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generic undo #175

Merged
merged 21 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions client/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/src/local_client/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub async fn run(mut game: Game, features: &Features) {
GameSyncRequest::None => {}
GameSyncRequest::ExecuteAction(a) => {
let p = game.active_player();
execute_action(&mut game, a, p);
game = execute_action(game, a, p);
state.show_player = game.active_player();
sync_result = GameSyncResult::Update;
}
Expand Down
43 changes: 43 additions & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ itertools = "0.14.0"
quad-rand = "0.2.3"
console_error_panic_hook = "0.1.7"
num = "0.4"
json-patch = "4.0.0"
36 changes: 4 additions & 32 deletions server/src/ability_initializer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use crate::content::custom_phase_actions::{
AdvanceRequest, CurrentEventHandler, CurrentEventRequest, CurrentEventResponse,
CurrentEventType, MultiRequest, PaymentRequest, PlayerRequest, PositionRequest,
ResourceRewardRequest, SelectedStructure, StructuresRequest, UnitTypeRequest, UnitsRequest,
AdvanceRequest, CurrentEventHandler, CurrentEventRequest, CurrentEventResponse, MultiRequest,
PaymentRequest, PlayerRequest, PositionRequest, ResourceRewardRequest, SelectedStructure,
StructuresRequest, UnitTypeRequest, UnitsRequest,
};
use crate::events::{Event, EventOrigin};
use crate::player_events::{CurrentEvent, PlayerCommands};
use crate::player_events::CurrentEvent;
use crate::position::Position;
use crate::resource_pile::ResourcePile;
use crate::undo::UndoContext;
use crate::unit::UnitType;
use crate::{content::custom_actions::CustomActionType, game::Game, player_events::PlayerEvents};
use std::collections::HashMap;
Expand Down Expand Up @@ -39,16 +38,6 @@ impl<'a, C, V> SelectedChoice<'a, C, V> {
details,
}
}

pub(crate) fn to_commands(
&self,
game: &mut Game,
gain: impl Fn(&mut PlayerCommands, &Game, &C) + 'static + Clone,
) {
game.with_commands(self.player_index, |commands, game| {
gain(commands, game, &self.choice);
});
}
}

pub struct AbilityListeners {
Expand Down Expand Up @@ -228,10 +217,6 @@ pub(crate) trait AbilityInitializerSetup: Sized {
.as_mut()
.expect("current missing")
.response = None;
if can_undo(&current.event_type) {
game.undo_context_stack
.push(UndoContext::Event(Box::new(current)));
}
let r = c.request.clone();
let a = action.clone();
phase.player.handler = None;
Expand Down Expand Up @@ -758,16 +743,3 @@ pub(crate) fn join_ability_initializers(setup: Vec<AbilityInitializer>) -> Abili
}
})
}

fn can_undo(event_type: &CurrentEventType) -> bool {
use CurrentEventType::*;
matches!(
event_type,
Advance(_)
| TurnStart
| Construct(_)
| Recruit(_)
| InfluenceCultureResolution(_)
| ExploreResolution(_)
)
}
73 changes: 32 additions & 41 deletions server/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ use crate::incident::trigger_incident;
use crate::log;
use crate::map::Terrain::Unexplored;
use crate::movement::{
has_movable_units, move_units_destinations, stop_current_move, take_move_state, CurrentMove,
MoveState,
has_movable_units, move_units_destinations, take_move_state, CurrentMove, MoveState,
};
use crate::playing_actions::PlayingAction;
use crate::recruit::on_recruit;
use crate::resource::check_for_waste;
use crate::resource_pile::ResourcePile;
use crate::status_phase::play_status_phase;
use crate::undo::{redo, undo, DisembarkUndoContext, UndoContext};
use crate::undo::{clean_patch, redo, to_serde_value, undo};
use crate::unit::MovementAction::{Move, Stop};
use crate::unit::{get_current_move, MovementAction};
use crate::wonder::draw_wonder_card;
Expand Down Expand Up @@ -78,36 +77,51 @@ impl Action {
/// # Panics
///
/// Panics if the action is illegal
pub fn execute_action(game: &mut Game, action: Action, player_index: usize) {
#[must_use]
pub fn execute_action(mut game: Game, action: Action, player_index: usize) -> Game {
let add_undo = !matches!(&action, Action::Undo);
let old = to_serde_value(&game);
let old_player = game.active_player();
game = execute_without_undo(game, action, player_index);
let new = to_serde_value(&game);
let new_player = game.active_player();
let patch = json_patch::diff(&new, &old);
if old_player != new_player {
game.lock_undo(); // don't undo player change
} else if add_undo && game.can_undo() {
game.action_log[game.action_log_index - 1].undo = clean_patch(patch.0);
}
game
}

fn execute_without_undo(mut game: Game, action: Action, player_index: usize) -> Game {
assert!(player_index == game.active_player(), "Illegal action");
if let Action::Undo = action {
assert!(
game.can_undo(),
"actions revealing new information can't be undone"
);
undo(game, player_index);
return;
return undo(game);
}

if matches!(action, Action::Redo) {
assert!(game.can_redo(), "no action can be redone");
redo(game, player_index);
return;
assert!(game.can_redo(), "action can't be redone");
redo(&mut game, player_index);
return game;
}

add_log_item_from_action(game, &action);
add_action_log_item(game, action.clone());
add_log_item_from_action(&mut game, &action);
add_action_log_item(&mut game, action.clone());

if let Some(s) = game.current_event_handler_mut() {
s.response = action.custom_phase_event();
let details = game.current_event().event_type.clone();
execute_custom_phase_action(game, player_index, &details);
execute_custom_phase_action(&mut game, player_index, &details);
} else {
execute_regular_action(game, action, player_index);
execute_regular_action(&mut game, action, player_index);
}
check_for_waste(game);

game.action_log[game.action_log_index - 1].undo = std::mem::take(&mut game.undo_context_stack);
check_for_waste(&mut game);
game
}

fn add_action_log_item(game: &mut Game, item: Action) {
Expand Down Expand Up @@ -138,7 +152,6 @@ pub(crate) fn execute_custom_phase_action(
start_combat(game);
}
CombatRoundEnd(r) => {
game.lock_undo();
if let Some(c) = combat_round_end(game, r) {
combat_loop(game, c);
}
Expand Down Expand Up @@ -202,28 +215,14 @@ pub(crate) fn execute_movement_action(
action: MovementAction,
player_index: usize,
) {
let Some(GameState::Movement(saved_state)) = game.state_stack.last().cloned() else {
panic!("game should be in a movement state");
};
let (starting_position, disembarked_units) = match action {
match action {
Move(m) => {
let player = &game.players[player_index];
let starting_position = player
.get_unit(*m.units.first().expect(
"instead of providing no units to move a stop movement actions should be done",
))
.position;
let disembarked_units = m
.units
.iter()
.filter_map(|unit| {
let unit = player.get_unit(*unit);
unit.carrier_id.map(|carrier_id| DisembarkUndoContext {
unit_id: unit.id,
carrier_id,
})
})
.collect();
match move_units_destinations(
player,
game,
Expand Down Expand Up @@ -283,26 +282,18 @@ pub(crate) fn execute_movement_action(
starting_position,
m.destination,
);
stop_current_move(game);
return; // can't undo this action
return;
}

if move_with_possible_combat(game, player_index, starting_position, &m) {
return;
}

(Some(starting_position), disembarked_units)
}
Stop => {
game.pop_state();
return;
}
};
game.push_undo_context(UndoContext::Movement {
starting_position,
move_state: saved_state,
disembarked_units,
});

let state = take_move_state(game);
let all_moves_used =
Expand Down
12 changes: 0 additions & 12 deletions server/src/advance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,18 +204,6 @@ pub(crate) fn gain_advance(game: &mut Game, player_index: usize, info: &AdvanceI
}
}

pub(crate) fn undo_advance(
game: &mut Game,
advance: &Advance,
player_index: usize,
was_custom_phase: bool,
) {
remove_advance(game, advance, player_index);
if !was_custom_phase {
game.players[player_index].incident_tokens += 1;
}
}

pub(crate) fn remove_advance(game: &mut Game, advance: &Advance, player_index: usize) {
(advance.listeners.deinitializer)(game, player_index);
(advance.listeners.undo_deinitializer)(game, player_index);
Expand Down
9 changes: 0 additions & 9 deletions server/src/city.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,6 @@ impl City {
self.angry_activation = false;
}

pub fn undo_activate(&mut self) {
self.activations -= 1;
if self.angry_activation {
self.angry_activation = false;
} else if self.is_activated() {
self.increase_mood_state();
}
}

#[must_use]
pub fn is_activated(&self) -> bool {
self.activations > 0
Expand Down
Loading