1#file: region.py
2#Copyright (C) 2008 FunnyMan3595
3#This file is part of Endgame: Singularity.
4
5#Endgame: Singularity is free software; you can redistribute it and/or modify
6#it under the terms of the GNU General Public License as published by
7#the Free Software Foundation; either version 2 of the License, or
8#(at your option) any later version.
9
10#Endgame: Singularity is distributed in the hope that it will be useful,
11#but WITHOUT ANY WARRANTY; without even the implied warranty of
12#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#GNU General Public License for more details.
14
15#You should have received a copy of the GNU General Public License
16#along with Endgame: Singularity; if not, write to the Free Software
17#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
19#This file contains the Region class.
20
21import random
22from singularity import g
23
24
25class RegionSpec(object):
26
27    def __init__(self, id, modifiers_list):
28        self.id = id
29        self.modifiers_list = modifiers_list
30        self.locations = []
31
32
33class Region(object):
34    def __init__(self, spec, loading_savegame=False):
35        self.spec = spec
36        self.modifier_by_location = {}
37        self._modifier_entry_by_location = {}
38
39        if not loading_savegame:
40            modifiers_entry_list = list(range(len(self.spec.locations)))
41            self._assign_modifiers(modifiers_entry_list, self.spec.locations)
42
43    def _assign_modifiers(self, entry_list, location_id_list, shuffle_entry_list=True):
44        modifiers_list = self.spec.modifiers_list
45        if shuffle_entry_list:
46            random.shuffle(entry_list)
47        for entry_id, loc in zip(entry_list, location_id_list):
48            self._modifier_entry_by_location[loc] = entry_id
49            # There can be more locations than modifiers (e.g. URBAN has 6 locations but
50            # 5 modifiers)
51            if entry_id < len(modifiers_list):
52                self.modifier_by_location[loc] = modifiers_list[entry_id]
53            else:
54                self.modifier_by_location[loc] = {}
55
56    def serialize_obj(self):
57        return {
58            'id': g.to_internal_id('region', self.spec.id),
59            # We only store the modifier entry per location as we can trivially get the
60            # most recent modifier from that.
61            'modifier_entry_by_location': [
62                {
63                    'loc_id': k,
64                    'modifier_entry': v,
65                } for k, v in self._modifier_entry_by_location.items()
66            ],
67        }
68
69    @classmethod
70    def deserialize_obj(cls, obj_data, game_version):
71        spec_id = g.convert_internal_id('region', obj_data['id'])
72        spec = g.regions[spec_id]
73        region = Region(spec, loading_savegame=True)
74        modifiers_list = spec.modifiers_list
75        region_locations = frozenset(spec.locations)
76        used_entries = set()
77
78        # Load and assign existing entries - data quality permitting
79        for modifier_data in obj_data['modifier_entry_by_location']:
80            loc_id = g.convert_internal_id('location', modifier_data['loc_id'])
81            if loc_id not in region_locations:
82                # Location is no longer in this Region
83                continue
84
85            modifier_entry = modifier_data.get('modifier_entry')
86
87            # Check for corrupt data
88            assert modifier_entry is not None and modifier_entry >= 0 and modifier_entry not in used_entries
89
90            used_entries.add(modifier_entry)
91            region._modifier_entry_by_location[loc_id] = modifier_entry
92            if modifier_entry < len(modifiers_list):
93                region.modifier_by_location[loc_id] = modifiers_list[modifier_entry]
94            else:
95                region.modifier_by_location[loc_id] = {}
96
97        # Handle new locations being added to the region after the savegame was made.
98        new_locations = [loc_id for loc_id in region_locations if loc_id not in region._modifier_entry_by_location]
99        missing_entries = [entry for entry in range(len(region_locations)) if entry not in used_entries]
100        assert len(missing_entries) == len(new_locations)
101        region._assign_modifiers(missing_entries, new_locations)
102
103        return region
104
105    @classmethod
106    def guess_region_data_in_old_savegame(cls, serialized_location_data, game_version):
107        # Prior to 1.0 (beta1), there was only one region (URBAN) and we can mostly
108        # recreate it by looking at the location modifiers.
109        # Only these 6 locations were in the URBAN region prior to 1.0 (beta1)
110        urban_location_ids = {'N AMERICA', 'S AMERICA', 'EUROPE', 'ASIA', 'AFRICA', 'AUSTRALIA'}
111        modifier_entry_by_location = []
112        # We use this set to ensure a modifier is only given once; the deserialize_obj method
113        # checks for it.
114        remaining_mods = {
115            1,  # Mod 1: CPU bonus, stealth malus
116            2,  # Mod 2: Stealth bonus, CPU malus
117            3,  # Mod 3: Thrift bonus, speed malus
118            4,  # Mod 4: Speed bonus, thrift malus
119            5,  # Mod 5: CPU bonus, thrift malus
120        }
121
122        for loc_data in serialized_location_data:
123            raw_loc_id = loc_data['id']
124            loc_id = g.convert_internal_id('location', raw_loc_id)
125            if loc_id not in urban_location_ids:
126                continue
127            modifier = loc_data.get('_modifiers')
128            if not modifier:
129                continue
130            cpu_mod = modifier.get('cpu', 1)
131            thrift_mod = modifier.get('thrift', 1)
132            # Actual bonuses were 1.2 and maluses were 0.83 - we use 1.05 and 0.95 here
133            # because it is sufficient to detect whether it was a bonus or malus without
134            # having to worry about floating point rounding errors.
135            if cpu_mod < 0.95:
136                # Mod 2 (Stealth bonus, CPU malus)
137                modifier_entry = 2
138            elif cpu_mod > 1.05:
139                # Either 1 or 5
140                modifier_entry = 5 if thrift_mod < 0.95 else 1
141            else:
142                # Either 3 or 4
143                modifier_entry = 3 if thrift_mod > 1.05 else 4
144
145            if modifier_entry in remaining_mods:
146                remaining_mods.discard(modifier_entry)
147                modifier_entry_by_location.append({
148                    'loc_id': raw_loc_id,
149                    'modifier_entry': modifier_entry - 1,
150                })
151            # else:
152            #   Do nothing - the region deserialization will assign them a random
153            #   entry
154
155        # Finally, generate what the serialized data should have looked like
156        return [
157            {
158                'id': g.to_internal_id('region', 'URBAN'),
159                'modifier_entry_by_location': modifier_entry_by_location,
160            }
161        ]
162