Skip to content

Commit

Permalink
MCR-103 Added documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisNeedham24 committed Feb 12, 2023
1 parent 897e8cc commit 50b4c61
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 5 deletions.
9 changes: 4 additions & 5 deletions source/saving/game_save_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
import pyxel

from source.display.board import Board
from source.saving.save_migrator import migrate_unit_plan, migrate_unit, migrate_player, migrate_climatic_effects, \
migrate_quad, migrate_settlement, migrate_game_config
from source.util.calculator import clamp
from source.foundation.catalogue import get_blessing, get_project, get_unit_plan, get_improvement
from source.foundation.models import Heathen, UnitPlan
from source.game_management.game_controller import GameController
from source.game_management.game_state import GameState
from source.foundation.models import Biome, Heathen, UnitPlan, AttackPlaystyle, AIPlaystyle, Unit, ExpansionPlaystyle, \
Faction
from source.saving.save_encoder import SaveEncoder, ObjectConverter
from source.saving.save_migrator import migrate_unit, migrate_player, migrate_climatic_effects, \
migrate_quad, migrate_settlement, migrate_game_config
from source.util.calculator import clamp

# The prefix attached to save files created by the autosave feature.
AUTOSAVE_PREFIX = "auto"
Expand Down
42 changes: 42 additions & 0 deletions source/saving/save_migrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@


def migrate_unit_plan(unit_plan) -> UnitPlan:
"""
Apply the heals attribute migration for UnitPlans, if required.
:param unit_plan: The loaded unit plan object.
:return: An optionally-migrated UnitPlan representation.
"""
plan_prereq = None if unit_plan.prereq is None else get_blessing(unit_plan.prereq.name)
will_heal: bool = unit_plan.heals if hasattr(unit_plan, "heals") else False

Expand All @@ -46,6 +51,13 @@ def migrate_unit_plan(unit_plan) -> UnitPlan:


def migrate_unit(unit) -> Unit:
"""
Apply the has_attacked to has_acted and sieging to besieging migrations for Units, if required.
:param unit: The loaded unit object.
:return: An optionally-migrated Unit representation.
"""
# Note for the below migrations that if we detect an outdated attribute, we migrate it and then delete it so that it
# does not pollute future saves.
will_have_acted: bool
if hasattr(unit, "has_acted"):
will_have_acted = unit.has_acted
Expand All @@ -63,6 +75,10 @@ def migrate_unit(unit) -> Unit:


def migrate_player(player):
"""
Apply the ai_playstyle, imminent_victories, faction, and eliminated migrations for Players, if required.
:param player: The loaded player object.
"""
if player.ai_playstyle is not None:
if hasattr(player.ai_playstyle, "attacking"):
player.ai_playstyle = AIPlaystyle(AttackPlaystyle[player.ai_playstyle.attacking],
Expand All @@ -76,11 +92,21 @@ def migrate_player(player):


def migrate_climatic_effects(game_state: GameState, save):
"""
Apply the night_status migrations for the game state, if required.
:param game_state: The state of the game being loaded in.
:param save: The loaded save data.
"""
game_state.until_night = save.night_status.until if hasattr(save, "night_status") else 0
game_state.nighttime_left = save.night_status.remaining if hasattr(save, "night_status") else 0


def migrate_quad(quad) -> Quad:
"""
Apply the is_relic migration for Quads, if required.
:param quad: The loaded quad object.
:return: An optionally-migrated Quad representation.
"""
new_quad = quad
# The biomes require special loading.
new_quad.biome = Biome[new_quad.biome]
Expand All @@ -89,24 +115,40 @@ def migrate_quad(quad) -> Quad:


def migrate_settlement(settlement):
"""
Apply the besieged migration for Settlements, if required.
:param settlement: The loaded settlement object.
"""
if not hasattr(settlement, "besieged"):
if settlement.under_siege_by is not None:
settlement.besieged = True
else:
settlement.besieged = False
# We now delete the old attribute so that it does not pollute future saves.
delattr(settlement, "under_siege_by")


def migrate_game_config(config) -> GameConfig:
"""
Apply the climatic_effects and player_faction migrations for game configuration, if required.
:param config: The loaded game configuration.
:return: An optionally-migrated GameConfig representation.
"""
if not hasattr(config, "climatic_effects"):
config.climatic_effects = False
if not hasattr(config, "player_faction"):
config.player_faction = get_faction_for_colour(config.player_colour)
# We now delete the old attribute so that it does not pollute future saves.
delattr(config, "player_colour")
return config


def get_faction_for_colour(colour: int) -> Faction:
"""
Utility function that retrieves the faction for the supplied colour. Used for colour-to-faction migrations.
:param colour: The colour to retrieve the faction for.
:return: The faction for the supplied colour.
"""
factions = list(FACTION_COLOURS.keys())
colours = list(FACTION_COLOURS.values())
idx = colours.index(colour)
Expand Down
63 changes: 63 additions & 0 deletions source/tests/test_save_migrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@


class SaveMigratorTest(unittest.TestCase):
"""
The test class for save_migrator.py.
"""

def test_unit_plan(self):
"""
Ensure that migrations occur correctly for UnitPlans.
"""
test_power = 100
test_max_health = 200
test_total_stamina = 4
test_name = "Bob"
test_cost = 350

# Simulate an up-to-date loaded unit plan.
test_loaded_plan: ObjectConverter = ObjectConverter({
"power": test_power,
"max_health": test_max_health,
Expand All @@ -32,6 +40,7 @@ def test_unit_plan(self):

migrated_plan: UnitPlan = migrate_unit_plan(test_loaded_plan)

# For up-to-date unit plans, the attributes should all map across directly.
self.assertEqual(test_power, migrated_plan.power)
self.assertEqual(test_max_health, migrated_plan.max_health)
self.assertEqual(test_total_stamina, migrated_plan.total_stamina)
Expand All @@ -41,16 +50,22 @@ def test_unit_plan(self):
self.assertFalse(migrated_plan.can_settle)
self.assertTrue(migrated_plan.heals)

# Now delete the heals attribute, to simulate an outdated save.
delattr(test_loaded_plan, "heals")

outdated_plan: UnitPlan = migrate_unit_plan(test_loaded_plan)
# Old unit plans should be mapped to False.
self.assertFalse(outdated_plan.heals)

def test_unit(self):
"""
Ensure that migrations occur correctly for Units.
"""
test_health = 300
test_remaining_stamina = 3
test_location = [1, 2]

# Simulate an up-to-date loaded unit.
test_loaded_unit: ObjectConverter = ObjectConverter({
"health": test_health,
"remaining_stamina": test_remaining_stamina,
Expand All @@ -63,6 +78,7 @@ def test_unit(self):

migrated_unit: Unit = migrate_unit(test_loaded_unit)

# For up-to-date units, the attributes should all map across directly.
self.assertEqual(test_health, migrated_unit.health)
self.assertEqual(test_remaining_stamina, migrated_unit.remaining_stamina)
self.assertTupleEqual((test_location[0], test_location[1]), migrated_unit.location)
Expand All @@ -71,23 +87,31 @@ def test_unit(self):
self.assertTrue(migrated_unit.has_acted)
self.assertFalse(migrated_unit.besieging)

# Now delete the has_acted and besieging attributes, replacing them with the outdated has_attacked and sieging
# attributes.
delattr(test_loaded_unit, "has_acted")
delattr(test_loaded_unit, "besieging")
test_loaded_unit.__dict__["has_attacked"] = True
test_loaded_unit.__dict__["sieging"] = False

outdated_unit: Unit = migrate_unit(test_loaded_unit)
# We expect the outdated attributes to be mapped to the new ones.
self.assertTrue(outdated_unit.has_acted)
self.assertFalse(outdated_unit.besieging)
# We also expect that the old attributes are deleted.
self.assertFalse(hasattr(outdated_unit, "has_attacked"))
self.assertFalse(hasattr(outdated_unit, "sieging"))

def test_player(self):
"""
Ensure that migrations occur correctly for players.
"""
test_attack_playstyle = AttackPlaystyle.AGGRESSIVE.value
test_expansion_playstyle = ExpansionPlaystyle.HERMIT.value
test_imminent_victories = [VictoryType.SERENDIPITY.value]
test_faction = Faction.FUNDAMENTALISTS.value

# Simulate an up-to-date loaded AI playstyle and an up-to-date loaded player.
test_loaded_ai_playstyle: ObjectConverter = ObjectConverter({
"attacking": test_attack_playstyle,
"expansion": test_expansion_playstyle
Expand All @@ -103,28 +127,39 @@ def test_player(self):

migrate_player(test_loaded_player)

# For up-to-date players, the attributes should all map across directly.
self.assertEqual(test_attack_playstyle, test_loaded_player.ai_playstyle.attacking)
self.assertEqual(test_expansion_playstyle, test_loaded_player.ai_playstyle.expansion)
self.assertSetEqual(set(test_imminent_victories), test_loaded_player.imminent_victories)
self.assertEqual(test_faction, test_loaded_player.faction)

# Now, convert the object to be like an outdated save, where AI playstyle only consisted of attacking, and the
# imminent victories, faction, and eliminated attributes did not exist.
test_loaded_player.__dict__["ai_playstyle"] = test_attack_playstyle
delattr(test_loaded_player, "imminent_victories")
delattr(test_loaded_player, "faction")
delattr(test_loaded_player, "eliminated")

migrate_player(test_loaded_player)

# We expect the attacking playstyle to be mapped across, and the expansion playstyle to be set to neutral.
self.assertEqual(test_attack_playstyle, test_loaded_player.ai_playstyle.attacking)
self.assertEqual(ExpansionPlaystyle.NEUTRAL, test_loaded_player.ai_playstyle.expansion)
# Imminent victories should be initialised to an empty set, since it'll be populated next turn anyway.
self.assertSetEqual(set(), test_loaded_player.imminent_victories)
# The player's faction should have been determined based on the player's colour.
self.assertEqual(test_faction, test_loaded_player.faction)
# Lastly, the player should not be eliminated, since they have a settlement.
self.assertFalse(test_loaded_player.eliminated)

def test_climatic_effects(self):
"""
Ensure that migrations occur correctly for game state.
"""
test_until_night = 3
test_nighttime_left = 0

# Simulate an up-to-date loaded save.
test_loaded_night_status: ObjectConverter = ObjectConverter({
"until": test_until_night,
"remaining": test_nighttime_left
Expand All @@ -136,57 +171,81 @@ def test_climatic_effects(self):

migrate_climatic_effects(test_game_state, test_loaded_save)

# For up-to-date saves, the attributes should be mapped directly.
self.assertEqual(test_game_state.until_night, test_until_night)
self.assertEqual(test_game_state.nighttime_left, test_nighttime_left)

# Now delete the night_status attribute, to simulate an outdated save from before the introduction of climatic
# effects.
delattr(test_loaded_save, "night_status")

migrate_climatic_effects(test_game_state, test_loaded_save)

# Since the day-night flow will not occur, we expect both to be initialised to zero.
self.assertFalse(test_game_state.until_night)
self.assertFalse(test_game_state.nighttime_left)

def test_quad(self):
"""
Ensure that migrations occur correctly for quads.
"""
test_biome = Biome.FOREST.value

# Simulate an up-to-date loaded quad.
test_loaded_quad: ObjectConverter = ObjectConverter({
"biome": test_biome,
"is_relic": True
})

migrated_quad: Quad = migrate_quad(test_loaded_quad)

# For up-to-date quads, we expect the attributes to be mapped over directly.
self.assertEqual(test_biome, migrated_quad.biome)
self.assertTrue(migrated_quad.is_relic)

# Now if we delete the is_relic attribute, we are replicating an outdated save.
delattr(test_loaded_quad, "is_relic")

outdated_quad: Quad = migrate_quad(test_loaded_quad)

# Even without the attribute, outdated quads should have is_relic set to False.
self.assertFalse(outdated_quad.is_relic)

def test_settlement(self):
"""
Ensure that migrations occur correctly for settlements.
"""
# Simulate an outdated loaded settlement under siege.
test_loaded_besieged_settlement: ObjectConverter = ObjectConverter({
"under_siege_by": Unit(1, 2, (3, 4), False, UNIT_PLANS[0])
})

migrate_settlement(test_loaded_besieged_settlement)

# The besieged attribute should have been determined based on the outdated under_siege_by attribute, which
# itself should also have been removed.
self.assertTrue(test_loaded_besieged_settlement.besieged)
self.assertFalse(hasattr(test_loaded_besieged_settlement, "under_siege_by"))

# Simulate an outdated loaded settlement that is not under siege.
test_loaded_settlement = ObjectConverter({
"under_siege_by": None
})

migrate_settlement(test_loaded_settlement)

# Once again, the besieged attribute should have been determined based on the under_siege_by attribute, which
# itself should also have been removed.
self.assertFalse(test_loaded_settlement.besieged)
self.assertFalse(hasattr(test_loaded_settlement, "under_siege_by"))

def test_game_config(self):
"""
Ensure that migrations occur correctly for game configuration.
"""
test_player_count = 9

# Simulate an outdated loaded game configuration.
test_loaded_config: ObjectConverter = ObjectConverter({
"player_count": test_player_count,
"player_colour": pyxel.COLOR_ORANGE,
Expand All @@ -196,9 +255,13 @@ def test_game_config(self):

outdated_config: GameConfig = migrate_game_config(test_loaded_config)

# Since this save was from before the introduction of climatic effects, it should have been mapped to False.
self.assertFalse(outdated_config.climatic_effects)
# The player faction should have been determined from the player_colour attribute, which should have been
# deleted.
self.assertEqual(Faction.FUNDAMENTALISTS, outdated_config.player_faction)
self.assertFalse(hasattr(outdated_config, "player_colour"))
# The other three unchanged attributes should have been mapped across directly.
self.assertEqual(test_player_count, outdated_config.player_count)
self.assertTrue(outdated_config.biome_clustering)
self.assertTrue(outdated_config.fog_of_war)
Expand Down

0 comments on commit 50b4c61

Please sign in to comment.