Skip to content

Anatomy of a Turn

billw2012 edited this page Dec 9, 2019 · 14 revisions

A break down of what the game does during a turn (to a fairly arbitrary level of detail).

CvGame::update() - the main entry point

  • Update plot paging
  • Optional first time initialization, happens once after a game is loaded (this should be moved somewhere more appropriate if possible, like the initialization functions)
    • plot groups hash update
    • great wall update
    • city visibility update
    • game option constraint enforcement
    • some fairly arbitrary looking caching of specific CvInfoBase type indices (we could probably just generate code for this to get lookup speed improvement for all types)
    • some other caching that should probably be done elsewhere, or lazily
  • Another one time initialization of player specific SM values (this is broken, see #275)
  • Begin time sliced turn

    • Python gameUpdate event is sent
    • Autosave happens if its the first time slice
    • If no player turns are started then Do Game Turn
    • Update score
    • Update Moves
    • For each alive player: Update Timers
    • Update turn timers, automatically ending player turns if they run out of time
    • Update all city plot assignments (where they are marked dirty)
    • Update player alive state based on game rule (Require Complete Kills being the main one)
    • If all human players are dead then Game Over now!
    • Increment turn slice counter
    • If not MP and active player has only automated units or didn't start their turn yet, and Minimize AI Turn is on, then loop back to Begin time sliced turn

Do Game Turn

  • Reset some global caches: shrines (?) and cultural victory
  • Update scores
  • Update and apply all properties
  • Deals update: verify they are valid, and update AI scores for them
  • For each team call Do Team Turn
  • Do Map Turn
  • Create barb cities
  • Create neanderthal cities (it uses a bool flag in the barb city function, fix this)
  • Create barb units
  • Create NPC spawns (after enough game turns have passed)
  • Apply global warming
  • Update religious holy cities (if the holy city was destroyed it moves here)
  • Update corporation headquarters (if the headquarters city was destroyed it moves here)
  • Do council votes
  • Reset all plot visibility (this is weird, and marked as a hack)
  • Python PreEndGameTurn event
  • Clear a couple of UI flags (end turn message and unit moved)
  • Update AI autoplay counters
  • Python EndGameTurn event
  • Increment game turn counter:
    • Update date
    • Recreate building commerce cache for all cities for all players (this can be cached as it would not change during a turn, however it could follow a lazy update pattern instead)
    • Set dirty flags: score, turn timer (UI flag), game data (UI flag)
  • Increment "elapsed" game turn counter (not sure how this differs from the normal game turn counter)
  • If doing MP simultaneous turns:
    • Set all players turns active (in a random order, I guess to remove order dependant benefits like building wonders first etc.)
  • Else if doing simultaneous team turns:
    • Set turn active for first team with an alive player
  • Else
    • Set turn active for first alive player (Pitboss is handled here but I'm not detailing it now)
  • Update Increasing Difficulty
  • Update Flexible Difficulty
  • Update Final Five
  • Update High to Low
  • Reset previous peace offer flags for all players
  • Found corporations (if Realistic Corporations is enabled)
  • Check for victory conditions
  • Set UI texture flag dirty (why here and now?)
  • Tell EXE to do turn (don't know what this actually does!)
  • Tell EXE to do autosave

Do Map Turn

  • Do turn for each map plot:
    • Update counters for ownership duration
    • Update improvement counter and apply upgrades
    • Heal defense damage
    • Clear bombard flag
    • Feature update:
      • Decay battle effects
      • Remove feature based on chance
      • Add feature based on chance
    • Culture update:
      • Apply culture from improvements (super forts)
      • Trigger revolts in undefended super forts (it only requires one defending unit to stop revolt entirely, this seems a bit gamey, it should be a probability modifier)
      • Trigger revolts in cities
    • Move all units to valid plots (or delete them if this is impossible - seems harsh, how about just teleport to capital?)
    • Apply claims from units
    • Apply resource depletion
    • Clear ownership of dead players

Do Team Turn

Called on a Team.

  • Increment some counters that indicate times that certain states have existed between teams (e.g. war, peace, contract, pacts etc.)
  • Do barbarian update (document this separately)
  • Decrement some counters that indicate how long certain states should remain between certain teams (espionage)
  • Update allow-tech-trading flag for every tech based on the tech brokering flag (this should be done more sensibly, this is overkill, why not just check the option as well when you check the flag?)
  • Decay war-weariness vs all other teams
  • Globe circumnavigation check
  • Update worst enemy of this team
  • Update team players area targets - each land area has a target city in it, this will be recalculated at 33% chance per turn (seems arbitrary, how about scaling this by game speed and making it more consistent?)
  • AI only: do war planning (document this separately)

Update Moves:

  • If doing MP simultaneous turns then randomize player ordering
  • For each alive player with turn active:
    • If player is human:
      • If the player has auto move flag set (they have some automated units that are yet to move, or are currently moving):
        • If the player has any groups awaiting orders then do AI Player Update now (it indicates that the player has ended their turn without giving orders to all groups, so we need to make sure to move any remaining automated units before moving to the next player)
        • Update Missions for all player unit groups
        • If no units are now busy then we can clear the auto moves flag for this player (allowing progress to the next player)
      • Otherwise:
        • If the player doesn't have any groups awaiting orders then do AI Player Update now (it indicates end of player turn, so we need to move all automated units now)
        • If the player has any units still awaiting orders then disallow automatic ending of the turn (related to the BUG option for auto end turn)
      • (WIP left off at CvGame.cpp line 8978 ?)
    • Otherwise:
      • Update Missions for all player unit groups
      • If the player doesn't have auto move flag set (they have not automated units that are yet to move, or are currently moving):
        • Do AI Player Update
        • If the player doesn't have any busy units and doesn't have any units ready to move then set the auto move flag
      • Otherwise:
        • If the player doesn't have any busy units then clear the auto move flag

Update Timers

Called on a player

  • For each group: (needs to be updated to use an invalidation safe iterator)
    • For each unit: (needs to be updated to use an invalidation safe iterator)
      • If the unit is in combat:
        • Do Update Combat
        • Don't update any more units unless DCM Stack Attack is enabled
    • If no combat happened then do Update Mission
    • Kill the group if it is marked for death
  • If there are more groups than units then kill any groups with pending delete (this is a bad hack, we just did a kill for all groups in the previous line, fix that instead)

Update Combat

This is a 2k+ line function doing the nitty gritty of actual combat actions, so this is a summary:

  • Decrement the combat timer, if it didn't hit zero yet then return (appears to be designed to allow time for animations to play, this might be an appropriate place to check for quick moves once, instead of in the numerous separate places I expect it is actually checked in)
  • If we just finished a round of combat then reselect the previous target
  • Otherwise select the best defender, if there is none found:
    • If we are doing move-attack then move to the target plot
    • Clear the attack mission and return
  • If we didn't just finish a round of combat:
    • If we aren't already fighting then set up combat (and apply some one off things like building defenses)
    • If the defender can't defend then kill the outright
    • Otherwise (if we aren't dead, it could happen during combat setup):
  • If we did finish a round of combat:
    • Apply post combat events (afflictions etc.)
    • If IDW is enabled do it now
    • If we died:
      • Apply healing to defender if there are adjacent units (random roll)
      • Do optional "Too Badass" escape
      • Do optional respawn
      • Do pillage if we won
    • Otherwise if the defender died:
      • Apply any War Weariness changes
      • Apply IDW for victory
      • Possible healing to attacker (and their group) if there are adjacent units (random roll)
      • Possible defender retreat
      • Possible defender respawn
      • Possible pillaging by attacker
      • Optional defender suicide or capture
      • Move attacking group into attacked plot if possible
      • Clear attacking group mission queue if group cannot move
    • Otherwise if the defender withdrew:
      • If attacking a city plot:
        • If attacker Stampede/Onslaught then do extra attack on target plot
        • Otherwise move the group if possible, and clear the mission if the group has no moves left
      • Otherwise if the retreat plot is valid:
        • Retreat the defender
        • If attacker Stampede/Onslaught then do extra attack on target plot
        • Otherwise move the group if possible, and clear the mission if the group has no moves left
      • Otherwise clear the mission if the group has no moves left
    • Otherwise if the attacker is repelled:
      • If attacker Stampede/Onslaught then do extra attack on target plot
      • Otherwise clear the mission if the group has no moves left
    • Otherwise if the defender is knocked back:
      • If the defender isn't in a city then retreat them to another plot
      • If attacker Stampede/Onslaught then do extra attack on target plot
      • Otherwise set defender ready to be captured by attacker (not sure what this is)
      • Move the group if possible, and clear the mission if the group has no moves left
    • Otherwise if the attacker withdrew:
      • If attacker Stampede/Onslaught then do extra attack on target plot
      • Otherwise clear the mission if the group has no moves left
    • Otherwise clear the mission if the group has no moves left NOTES: Obviously there is some structural refactoring that would simplify this function a lot.

Resolve Combat

Called on a unit (attacker) against another unit (defender). A big function, only summary of logic is presented here.

  • Calculate a bunch of stats like initial strength, probabilities of various things happening etc.
  • If Vanilla Combat Engine setting is enabled then we calculate basic combat values here once
  • Apply collateral combat damage
  • Loop:
    • If Vanilla Combat Engine is not enabled we recalculate basic combat values here (every iteration)
    • Calculate a bunch of per iteration combat values (dice rolls)
    • Do Breakdown if appropriate
    • Defender round:
      • Possible attacker withdrawal
      • Apply damage to attacker
      • Status checks (stun, crit, cold)
      • Ranged first strikes
      • More status checks (repel etc.)
      • Afflictions
    • Attacker round:
      • Possible attacker withdrawal due to damage, break combat loop
      • Possible defender withdrawal, exit combat
      • Apply damage to defender
      • Status checks (stun, crit, cold)
      • Ranged first strikes
      • Knockback and attacker affliction
    • Update available actions (decreasing a bunch of counters)
    • If attacker is dead:
      • Give defender xp
      • Break combat loop
    • Otherwise if defender is dead:
      • Apply flanking damage
      • Give attacker xp
    • Apply battlefield promotions
    • Apply dynamic xp if enabled
    • Apply commerce attacks (reduces commerce in city)

AI Player Update

Runs on player, updates player units Introduces something called the group cycle. (Not sure what it is yet!)

  • Do delayed death for all selection groups
  • If the player doesn't have a unit currently busy:
    • Split up all groups in the group cycle
    • Do AI Group Update for each group in the group cycle (in movement priority order if the player is AI, otherwise in default order)

AI Group Update

Called on groups.

  • Return immediately if the group isn't AI controlled (owned by non-human player, OR automated by player)
  • Set force update flag if non-human player and not cargo (so AI will reevaluate best mission for units every turn)
  • If forcing update:
    • Clear current mission
    • Set activity to awake
    • Clear force update flag
    • Cancel any in progress group attack (how can it be possible to be here while in the middle of a group attack? Its a bit weird)
  • Reset caches for edge costs (pathfinding)
  • While the group is doing a group attack or has units ready to move:
    • If the group attack flag is set
      • Clear group attack flag
      • Do direct Group Attack (will set the flag again if an attack should continue)
    • Otherwise:
      • Reset pathfinding
      • Do a standard AI Unit Update on the group head unit
      • Might break from the while loop now if the AI Unit Update requires waiting until the next time slice
    • Do delayed death, if it happened then exit loop now
    • If not doing a group attack and the force separate group flag was set, then separate the group now (split the group up so individual units can get new orders)
  • If the group is still alive, not human and not awaiting a contract result (either tender or search)
    • If not doing a group attack try and do any mission any unit in the group can do (hopefully this is a last resort fallback that rarely happens, also maybe whether units should always try and do something even without some higher level plan should be reconsidered, it's not how humans will play)
    • If the group still has moves left then skip turn

AI Unit Update

Called on a unit.

  • If the unit can't move:
  • If the group isn't busy then push a skip mission for this unit (the unit won't be able to help this turn)
  • Return
  • Mark the group awake
  • Call Python update callback, if successful then return now (this is one we should remove, AI should just be restricted to the DLL, for many reasons)
  • If its a land unit:
    • If it is on water and can't move on water then skip turn and return
    • Otherwise if it is in a transport that already moved or has a mission assigned then also skip turn and return (the unit is being taken somewhere so wait for the transport to get there)
  • If the unit already attacked this turn, then try and do a free action (bombard etc.), and return
  • If the group is automated call its automation AI (there are loads of them, for workers, explorers etc.)
  • Otherwise call the base AI unit move for the units AI type (again there are loads)
  • Update unit garrison assignment (if the unit did something else then it gets unmarked as part of a garrison)

Group Attack

This function and DCM Group Stack Attack look basically identical...

  • If DCM Stack Attack is enabled then do DCM Group Stack Attack and return
  • If this is a move-attack action we regenerate the path to the target plot (which might be non-adjacent) and select the first plot in the path as our target for this function
  • If we can actually attack (target plot is adjacent or we are air units, flags are set correct etc.)
    • If we have a best attacker:
      • Select best defender in target plot, revealing a stealth defender if necessary
      • Abort now if human and target plot isn't visible (we can do this earlier in the function...)
      • Declare war now if we cannot enter the target plot without doing so, and are ready to do so (plan is ready etc.)
      • Mark target plot for battle effects (plot damage)
      • If DCM Attack Support is enabled then do it now for attackers and defenders (bombardment, air-strikes etc.)
      • Attack Loop (what is the actual intent of this loop?):
        • If we don't have a valid attacker then we are done
        • If there is no defender, try and reveal a stealth defender
        • If the attack odds of the current attacker are < 68 (percent I guess?) and it's not a stealth unit defending and not a human attacking (why?):
          • If there are no bombard units left then choose a sacrificial unit as the attacker
          • Otherwise do a round of bombardment and restart the loop
        • Callback to Python doCombat if it is enabled. If this happens then the loop is exited (this call is probably incoherent, it doesn't appear to consider any of the stuff we just calculated above, perhaps we can remove it?)
        • If there is a defender then attack the plot
        • If attacking unit is now fighting:
          • If human, then remove attacking unit from their group (so further attack can be done with the remaining units)
          • If automated or not human, and attacking unit is now fighting, set the group attack flag
          • Exit Attack Loop
        • Continue Attack Loop

DCM Group Stack Attack

Code in this is particularly incoherent, after refactoring this explanation will probably be a lot simpler.

  • If attack is possible this turn (within range etc.):
    • Select best attacker
      • If there is no non-stealth defender then reveal the best stealth defender for the current attacker (only if the target plot isn't a city though?)
        • This locks in the first attacker
      • If attacker is human then abort now if the target plot isn't visible, unless using air units (this could be done way earlier)
      • Select best defender
      • Declare war now if we cannot enter the target plot without doing so, and are ready to do so (plan is ready etc.)
      • Mark target plot for battle effects (plot damage)
      • If DCM Attack Support is enabled then do it now for attackers and defenders (bombardment, air-strikes etc.)
      • Attack loop:
        • Select best attacker unless we locked it in already above due to a stealth unit being revealed (not sure why this is logically correct though? perhaps it is redundant or a premature optimization)
        • If we don't have a valid attacker then we are done
        • If there is no defender, try and reveal a stealth defender
        • If the attack odds of the current attacker are < 68 (percent I guess?) and it's not a stealth unit defending and not a human attacking (why?):
          • If there are no bombard units left then choose a sacrificial unit as the attacker
          • Otherwise do a round of bombardment and restart the loop
        • Callback to Python doCombat if it is enabled. If this happens then the loop is exited (this call is probably incoherent, it doesn't appear to consider any of the stuff we just calculated above, perhaps we can remove it?)
        • Attack plot with selected attacker.
        • If the group is AI or human automated then set group attack flag on the group and exit

Update Missions

Runs on a unit group

  • If the group is in danger then clear the mission so the player can reconsider (e.g. workers that wake up when enemy are near)
  • Otherwise if the mission isn't started then start it:
    • Flag the group as either doing a mission or about to do one (if not all the units can move currently)
    • If the mission is actually doable:
    • For a lot of missions only the activity state of the group needs to be set (e.g. sleep, heal, sentry, etc.)
    • Otherwise if its a Pillage mission there is custom behaviour here
    • For other mission types that involve moving:
      • Best unit is selected for the mission if applicable (only ambush uses this)
      • Perform mission for each unit in the group (or only the best if one was selected)
      • Most missions here are implemented as a unit function, there are many (probably mission implementations should be a type, implementing them on the unit itself is clunky and inflexible)
    • If the plot is visible on screen then update mission timer for animation
    • If nuke was exploded then always set animation timer (probably we always want it visible for player)
    • If the group is now NOT busy (i.e. in the middle of an attack animation) then delete the mission if it is over, or continue it as below
  • Otherwise continue the mission:
    • (lots of error condition checks that should be asserts instead)
    • If its a move mission and all units can move:
    • If the mission was created on this turn or flagged to ignore enemy then just do the move and all units can move then do it
    • Else if the mission is flagged to reconsider when leaving players territory or nearing enemy units then do the move if safe, otherwise abort the mission
    • If all units can move then try and do your mission (move missions of a few kinds are implemented here, including unit pickup)
    • Show moves to player if appropriate
    • If the mission was completed:
    • Select next unit if human player is doing the moving
    • Pop the mission off the queue
    • If no units in this group can move and there isn't already a mission pushed, then push a skip mission
    • Otherwise:
    • If all the units in the group can still move then jump back up and continue the mission again, incrementing the step count
    • Otherwise if the group isn't busy (animations) and human player is active then cycle the selection (so player can move the next unit)