Пример #1
0
class Game:
    def __init__(
        self,
        player_faction: Faction,
        enemy_faction: Faction,
        theater: ConflictTheater,
        start_date: datetime,
        settings: Settings,
        player_budget: float,
        enemy_budget: float,
    ) -> None:
        self.settings = settings
        self.events: List[Event] = []
        self.theater = theater
        self.player_faction = player_faction
        self.player_country = player_faction.country
        self.enemy_faction = enemy_faction
        self.enemy_country = enemy_faction.country
        # pass_turn() will be called when initialization is complete which will
        # increment this to turn 0 before it reaches the player.
        self.turn = -1
        # NB: This is the *start* date. It is never updated.
        self.date = date(start_date.year, start_date.month, start_date.day)
        self.game_stats = GameStats()
        self.notes = ""
        self.ground_planners: dict[int, GroundPlanner] = {}
        self.informations = []
        self.informations.append(Information("Game Start", "-" * 40, 0))
        # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
        self.__culling_zones: List[Point] = []
        self.__destroyed_units: list[dict[str, Union[float, str]]] = []
        self.savepath = ""
        self.budget = player_budget
        self.enemy_budget = enemy_budget
        self.current_unit_id = 0
        self.current_group_id = 0
        self.name_generator = naming.namegen

        self.conditions = self.generate_conditions()

        self.blue_transit_network = TransitNetwork()
        self.red_transit_network = TransitNetwork()

        self.blue_procurement_requests: List[AircraftProcurementRequest] = []
        self.red_procurement_requests: List[AircraftProcurementRequest] = []

        self.blue_ato = AirTaskingOrder()
        self.red_ato = AirTaskingOrder()

        self.blue_bullseye = Bullseye(Point(0, 0))
        self.red_bullseye = Bullseye(Point(0, 0))

        self.aircraft_inventory = GlobalAircraftInventory(
            self.theater.controlpoints)

        self.transfers = PendingTransfers(self)

        self.sanitize_sides()

        self.blue_faker = Faker(self.player_faction.locales)
        self.red_faker = Faker(self.enemy_faction.locales)

        self.blue_air_wing = AirWing(self, player=True)
        self.red_air_wing = AirWing(self, player=False)

        self.on_load(game_still_initializing=True)

    def __getstate__(self) -> dict[str, Any]:
        state = self.__dict__.copy()
        # Avoid persisting any volatile types that can be deterministically
        # recomputed on load for the sake of save compatibility.
        del state["blue_threat_zone"]
        del state["red_threat_zone"]
        del state["blue_navmesh"]
        del state["red_navmesh"]
        del state["blue_faker"]
        del state["red_faker"]
        return state

    def __setstate__(self, state: dict[str, Any]) -> None:
        self.__dict__.update(state)
        # Regenerate any state that was not persisted.
        self.on_load()

    def ato_for(self, player: bool) -> AirTaskingOrder:
        if player:
            return self.blue_ato
        return self.red_ato

    def procurement_requests_for(
            self, player: bool) -> List[AircraftProcurementRequest]:
        if player:
            return self.blue_procurement_requests
        return self.red_procurement_requests

    def transit_network_for(self, player: bool) -> TransitNetwork:
        if player:
            return self.blue_transit_network
        return self.red_transit_network

    def generate_conditions(self) -> Conditions:
        return Conditions.generate(self.theater, self.current_day,
                                   self.current_turn_time_of_day,
                                   self.settings)

    def sanitize_sides(self) -> None:
        """
        Make sure the opposing factions are using different countries
        :return:
        """
        if self.player_country == self.enemy_country:
            if self.player_country == "USA":
                self.enemy_country = "USAF Aggressors"
            elif self.player_country == "Russia":
                self.enemy_country = "USSR"
            else:
                self.enemy_country = "Russia"

    def faction_for(self, player: bool) -> Faction:
        if player:
            return self.player_faction
        return self.enemy_faction

    def faker_for(self, player: bool) -> Faker:
        if player:
            return self.blue_faker
        return self.red_faker

    def air_wing_for(self, player: bool) -> AirWing:
        if player:
            return self.blue_air_wing
        return self.red_air_wing

    def country_for(self, player: bool) -> str:
        if player:
            return self.player_country
        return self.enemy_country

    def bullseye_for(self, player: bool) -> Bullseye:
        if player:
            return self.blue_bullseye
        return self.red_bullseye

    def _generate_player_event(self, event_class: Type[Event],
                               player_cp: ControlPoint,
                               enemy_cp: ControlPoint) -> None:
        self.events.append(
            event_class(
                self,
                player_cp,
                enemy_cp,
                enemy_cp.position,
                self.player_faction.name,
                self.enemy_faction.name,
            ))

    def _generate_events(self) -> None:
        for front_line in self.theater.conflicts():
            self._generate_player_event(
                FrontlineAttackEvent,
                front_line.blue_cp,
                front_line.red_cp,
            )

    def adjust_budget(self, amount: float, player: bool) -> None:
        if player:
            self.budget += amount
        else:
            self.enemy_budget += amount

    def process_player_income(self) -> None:
        self.budget += Income(self, player=True).total

    def process_enemy_income(self) -> None:
        # TODO: Clean up save compat.
        if not hasattr(self, "enemy_budget"):
            self.enemy_budget = 0
        self.enemy_budget += Income(self, player=False).total

    @staticmethod
    def initiate_event(event: Event) -> UnitMap:
        # assert event in self.events
        logging.info("Generating {} (regular)".format(event))
        return event.generate()

    def finish_event(self, event: Event, debriefing: Debriefing) -> None:
        logging.info("Finishing event {}".format(event))
        event.commit(debriefing)

        if event in self.events:
            self.events.remove(event)
        else:
            logging.info("finish_event: event not in the events!")

    def on_load(self, game_still_initializing: bool = False) -> None:
        if not hasattr(self, "name_generator"):
            self.name_generator = naming.namegen
        # Hack: Replace the global name generator state with the state from the save
        # game.
        #
        # We need to persist this state so that names generated after game load don't
        # conflict with those generated before exit.
        naming.namegen = self.name_generator
        LuaPluginManager.load_settings(self.settings)
        ObjectiveDistanceCache.set_theater(self.theater)
        self.compute_conflicts_position()
        if not game_still_initializing:
            self.compute_threat_zones()
        self.blue_faker = Faker(self.faction_for(player=True).locales)
        self.red_faker = Faker(self.faction_for(player=False).locales)

    def reset_ato(self) -> None:
        self.blue_ato.clear()
        self.red_ato.clear()

    def finish_turn(self, skipped: bool = False) -> None:
        """Finalizes the current turn and advances to the next turn.

        This handles the turn-end portion of passing a turn. Initialization of the next
        turn is handled by `initialize_turn`. These are separate processes because while
        turns may be initialized more than once under some circumstances (see the
        documentation for `initialize_turn`), `finish_turn` performs the work that
        should be guaranteed to happen only once per turn:

        * Turn counter increment.
        * Delivering units ordered the previous turn.
        * Transfer progress.
        * Squadron replenishment.
        * Income distribution.
        * Base strength (front line position) adjustment.
        * Weather/time-of-day generation.

        Some actions (like transit network assembly) will happen both here and in
        `initialize_turn`. We need the network to be up to date so we can account for
        base captures when processing the transfers that occurred last turn, but we also
        need it to be up to date in the case of a re-initialization in `initialize_turn`
        (such as to account for a cheat base capture) so that orders are only placed
        where a supply route exists to the destination. This is a relatively cheap
        operation so duplicating the effort is not a problem.

        Args:
            skipped: True if the turn was skipped.
        """
        self.informations.append(
            Information("End of turn #" + str(self.turn), "-" * 40, 0))
        self.turn += 1

        # Need to recompute before transfers and deliveries to account for captures.
        # This happens in in initialize_turn as well, because cheating doesn't advance a
        # turn but can capture bases so we need to recompute there as well.
        self.compute_transit_networks()

        # Must happen *before* unit deliveries are handled, or else new units will spawn
        # one hop ahead. ControlPoint.process_turn handles unit deliveries.
        self.transfers.perform_transfers()

        # Needs to happen *before* planning transfers so we don't cancel them.
        self.reset_ato()
        for control_point in self.theater.controlpoints:
            control_point.process_turn(self)

        self.blue_air_wing.replenish()
        self.red_air_wing.replenish()

        if not skipped:
            for cp in self.theater.player_points():
                cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
        elif self.turn > 1:
            for cp in self.theater.player_points():
                if not cp.is_carrier and not cp.is_lha:
                    cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)

        self.conditions = self.generate_conditions()

        self.process_enemy_income()
        self.process_player_income()

    def begin_turn_0(self) -> None:
        """Initialization for the first turn of the game."""
        self.turn = 0
        self.initialize_turn()

    def pass_turn(self, no_action: bool = False) -> None:
        """Ends the current turn and initializes the new turn.

        Called both when skipping a turn or by ending the turn as the result of combat.

        Args:
            no_action: True if the turn was skipped.
        """
        logging.info("Pass turn")
        with logged_duration("Turn finalization"):
            self.finish_turn(no_action)
        with logged_duration("Turn initialization"):
            self.initialize_turn()

        # Autosave progress
        persistency.autosave(self)

    def check_win_loss(self) -> TurnState:
        player_airbases = {
            cp
            for cp in self.theater.player_points()
            if cp.runway_is_operational()
        }
        if not player_airbases:
            return TurnState.LOSS

        enemy_airbases = {
            cp
            for cp in self.theater.enemy_points()
            if cp.runway_is_operational()
        }
        if not enemy_airbases:
            return TurnState.WIN

        return TurnState.CONTINUE

    def set_bullseye(self) -> None:
        player_cp, enemy_cp = self.theater.closest_opposing_control_points()
        self.blue_bullseye = Bullseye(enemy_cp.position)
        self.red_bullseye = Bullseye(player_cp.position)

    def initialize_turn(self,
                        for_red: bool = True,
                        for_blue: bool = True) -> None:
        """Performs turn initialization for the specified players.

        Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
        processing happens in `pass_turn` (despite the name, it's called both for
        skipping the turn and ending the turn after combat).

        Special care needs to be taken here because initialization can occur more than
        once per turn. A number of events can require re-initializing a turn:

        * Cheat capture. Bases changing hands invalidates many missions in both ATOs,
          purchase orders, threat zones, transit networks, etc. Practically speaking,
          after a base capture the turn needs to be treated as fully new. The game might
          even be over after a capture.
        * Cheat front line position. CAS missions are no longer in the correct location,
          and the ground planner may also need changes.
        * Selling/buying units at TGOs. Selling a TGO might leave missions in the ATO
          with invalid targets. Buying a new SAM (or even replacing some units in a SAM)
          potentially changes the threat zone and may alter mission priorities and
          flight planning.

        Most of the work is delegated to initialize_turn_for, which handles the
        coalition-specific turn initialization. In some cases only one coalition will be
        (re-) initialized. This is the case when buying or selling TGO units, since we
        don't want to force the player to redo all their planning just because they
        repaired a SAM, but should replan opfor when that happens. On the other hand,
        base captures are significant enough (and likely enough to be the first thing
        the player does in a turn) that we replan blue as well. Front lines are less
        impactful but also likely to be early, so they also cause a blue replan.

        Args:
            for_red: True if opfor should be re-initialized.
            for_blue: True if the player coalition should be re-initialized.
        """
        self.events = []
        self._generate_events()
        self.set_bullseye()

        # Update statistics
        self.game_stats.update(self)

        # Check for win or loss condition
        turn_state = self.check_win_loss()
        if turn_state in (TurnState.LOSS, TurnState.WIN):
            return self.process_win_loss(turn_state)

        # Plan Coalition specific turn
        if for_red:
            self.initialize_turn_for(player=False)
        if for_blue:
            self.initialize_turn_for(player=True)

        # Plan GroundWar
        for cp in self.theater.controlpoints:
            if cp.has_frontline:
                gplanner = GroundPlanner(cp, self)
                gplanner.plan_groundwar()
                self.ground_planners[cp.id] = gplanner

    def initialize_turn_for(self, player: bool) -> None:
        """Processes coalition-specific turn initialization.

        For more information on turn initialization in general, see the documentation
        for `Game.initialize_turn`.

        Args:
            player: True if the player coalition is being initialized. False for opfor
            initialization.
        """
        self.ato_for(player).clear()
        self.air_wing_for(player).reset()

        self.aircraft_inventory.reset()
        for cp in self.theater.controlpoints:
            self.aircraft_inventory.set_from_control_point(cp)
            # Refund all pending deliveries for opfor and if player
            # has automate_aircraft_reinforcements
            if (not player and not cp.captured) or (
                    player and cp.captured
                    and self.settings.automate_aircraft_reinforcements):
                cp.pending_unit_deliveries.refund_all(self)

        # Plan flights & combat for next turn
        with logged_duration("Computing conflict positions"):
            self.compute_conflicts_position()
        with logged_duration("Threat zone computation"):
            self.compute_threat_zones()
        with logged_duration("Transit network identification"):
            self.compute_transit_networks()
        self.ground_planners = {}

        self.procurement_requests_for(player).clear()

        with logged_duration("Procurement of airlift assets"):
            self.transfers.order_airlift_assets()
        with logged_duration("Transport planning"):
            self.transfers.plan_transports()

        if not player or (player and self.settings.auto_ato_behavior
                          is not AutoAtoBehavior.Disabled):
            color = "Blue" if player else "Red"
            with logged_duration(f"{color} mission planning"):
                mission_planner = CoalitionMissionPlanner(self, player)
                mission_planner.plan_missions()

        self.plan_procurement_for(player)

    def plan_procurement_for(self, for_player: bool) -> None:
        # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
        # gets much more of the budget that turn. Otherwise budget (after
        # repairs) is split evenly between air and ground. For the default
        # starting budget of 2000 this gives 600 to ground forces and 1400 to
        # aircraft. After that the budget will be spend proportionally based on how much is already invested

        if for_player:
            self.budget = ProcurementAi(
                self,
                for_player=True,
                faction=self.player_faction,
                manage_runways=self.settings.automate_runway_repair,
                manage_front_line=self.settings.
                automate_front_line_reinforcements,
                manage_aircraft=self.settings.automate_aircraft_reinforcements,
            ).spend_budget(self.budget)
        else:
            self.enemy_budget = ProcurementAi(
                self,
                for_player=False,
                faction=self.enemy_faction,
                manage_runways=True,
                manage_front_line=True,
                manage_aircraft=True,
            ).spend_budget(self.enemy_budget)

    def message(self, text: str) -> None:
        self.informations.append(Information(text, turn=self.turn))

    @property
    def current_turn_time_of_day(self) -> TimeOfDay:
        return list(TimeOfDay)[self.turn % 4]

    @property
    def current_day(self) -> date:
        return self.date + timedelta(days=self.turn // 4)

    def next_unit_id(self) -> int:
        """
        Next unit id for pre-generated units
        """
        self.current_unit_id += 1
        return self.current_unit_id

    def next_group_id(self) -> int:
        """
        Next unit id for pre-generated units
        """
        self.current_group_id += 1
        return self.current_group_id

    def compute_transit_networks(self) -> None:
        self.blue_transit_network = self.compute_transit_network_for(
            player=True)
        self.red_transit_network = self.compute_transit_network_for(
            player=False)

    def compute_transit_network_for(self, player: bool) -> TransitNetwork:
        return TransitNetworkBuilder(self.theater, player).build()

    def compute_threat_zones(self) -> None:
        self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
        self.red_threat_zone = ThreatZones.for_faction(self, player=False)
        self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
                                                      self.theater)
        self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
                                                     self.theater)

    def threat_zone_for(self, player: bool) -> ThreatZones:
        if player:
            return self.blue_threat_zone
        return self.red_threat_zone

    def navmesh_for(self, player: bool) -> NavMesh:
        if player:
            return self.blue_navmesh
        return self.red_navmesh

    def compute_conflicts_position(self) -> None:
        """
        Compute the current conflict center position(s), mainly used for culling calculation
        :return: List of points of interests
        """
        zones = []

        # By default, use the existing frontline conflict position
        for front_line in self.theater.conflicts():
            position = Conflict.frontline_position(front_line, self.theater)
            zones.append(position[0])
            zones.append(front_line.blue_cp.position)
            zones.append(front_line.red_cp.position)

        for cp in self.theater.controlpoints:
            # If do_not_cull_carrier is enabled, add carriers as culling point
            if self.settings.perf_do_not_cull_carrier:
                if cp.is_carrier or cp.is_lha:
                    zones.append(cp.position)

        # If there is no conflict take the center point between the two nearest opposing bases
        if len(zones) == 0:
            cpoint = None
            min_distance = math.inf
            for cp in self.theater.player_points():
                for cp2 in self.theater.enemy_points():
                    d = cp.position.distance_to_point(cp2.position)
                    if d < min_distance:
                        min_distance = d
                        cpoint = Point(
                            (cp.position.x + cp2.position.x) / 2,
                            (cp.position.y + cp2.position.y) / 2,
                        )
                        zones.append(cp.position)
                        zones.append(cp2.position)
                        break
                if cpoint is not None:
                    break
            if cpoint is not None:
                zones.append(cpoint)

        packages = itertools.chain(self.blue_ato.packages,
                                   self.red_ato.packages)
        for package in packages:
            if package.primary_task is FlightType.BARCAP:
                # BARCAPs will be planned at most locations on smaller theaters,
                # rendering culling fairly useless. BARCAP packages don't really
                # need the ground detail since they're defensive. SAMs nearby
                # are only interesting if there are enemies in the area, and if
                # there are they won't be culled because of the enemy's mission.
                continue
            zones.append(package.target.position)

        # Else 0,0, since we need a default value
        # (in this case this means the whole map is owned by the same player, so it is not an issue)
        if len(zones) == 0:
            zones.append(Point(0, 0))

        self.__culling_zones = zones

    def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
        pos = Point(cast(float, data["x"]), cast(float, data["z"]))
        if self.theater.is_on_land(pos):
            self.__destroyed_units.append(data)

    def get_destroyed_units(self) -> list[dict[str, Union[float, str]]]:
        return self.__destroyed_units

    def position_culled(self, pos: Point) -> bool:
        """
        Check if unit can be generated at given position depending on culling performance settings
        :param pos: Position you are tryng to spawn stuff at
        :return: True if units can not be added at given position
        """
        if not self.settings.perf_culling:
            return False
        for z in self.__culling_zones:
            if z.distance_to_point(
                    pos) < self.settings.perf_culling_distance * 1000:
                return False
        return True

    def get_culling_zones(self) -> list[Point]:
        """
        Check culling points
        :return: List of culling zones
        """
        return self.__culling_zones

    # 1 = red, 2 = blue
    def get_player_coalition_id(self) -> int:
        return 2

    def get_enemy_coalition_id(self) -> int:
        return 1

    def get_player_coalition(self) -> Coalition:
        return Coalition.Blue

    def get_enemy_coalition(self) -> Coalition:
        return Coalition.Red

    def get_player_color(self) -> str:
        return "blue"

    def get_enemy_color(self) -> str:
        return "red"

    def process_win_loss(self, turn_state: TurnState) -> None:
        if turn_state is TurnState.WIN:
            self.message(
                "Congratulations, you are victorious! Start a new campaign to continue."
            )
        elif turn_state is TurnState.LOSS:
            self.message(
                "Game Over, you lose. Start a new campaign to continue.")
Пример #2
0
class Game:
    def __init__(
        self,
        player_name: str,
        enemy_name: str,
        theater: ConflictTheater,
        start_date: datetime,
        settings: Settings,
        player_budget: float,
        enemy_budget: float,
    ) -> None:
        self.settings = settings
        self.events: List[Event] = []
        self.theater = theater
        self.player_name = player_name
        self.player_country = db.FACTIONS[player_name].country
        self.enemy_name = enemy_name
        self.enemy_country = db.FACTIONS[enemy_name].country
        self.turn = 0
        self.date = date(start_date.year, start_date.month, start_date.day)
        self.game_stats = GameStats()
        self.game_stats.update(self)
        self.ground_planners: Dict[int, GroundPlanner] = {}
        self.informations = []
        self.informations.append(Information("Game Start", "-" * 40, 0))
        # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
        self.__culling_zones: List[Point] = []
        # Culling Points are for individual theater ground objects that we don't wish to cull.
        self.__culling_points: List[Point] = []
        self.__destroyed_units: List[str] = []
        self.savepath = ""
        self.budget = player_budget
        self.enemy_budget = enemy_budget
        self.current_unit_id = 0
        self.current_group_id = 0

        self.conditions = self.generate_conditions()

        self.blue_ato = AirTaskingOrder()
        self.red_ato = AirTaskingOrder()

        self.aircraft_inventory = GlobalAircraftInventory(
            self.theater.controlpoints)

        self.sanitize_sides()

        self.on_load()

        # Turn 0 procurement. We don't actually have any missions to plan, but
        # the planner will tell us what it would like to plan so we can use that
        # to drive purchase decisions.
        blue_planner = CoalitionMissionPlanner(self, is_player=True)
        blue_planner.plan_missions()

        red_planner = CoalitionMissionPlanner(self, is_player=False)
        red_planner.plan_missions()

        self.plan_procurement(blue_planner, red_planner)

    def __getstate__(self) -> Dict[str, Any]:
        state = self.__dict__.copy()
        # Avoid persisting any volatile types that can be deterministically
        # recomputed on load for the sake of save compatibility.
        del state["blue_threat_zone"]
        del state["red_threat_zone"]
        del state["blue_navmesh"]
        del state["red_navmesh"]
        return state

    def __setstate__(self, state: Dict[str, Any]) -> None:
        self.__dict__.update(state)
        # Regenerate any state that was not persisted.
        self.on_load()

    def generate_conditions(self) -> Conditions:
        return Conditions.generate(self.theater, self.date,
                                   self.current_turn_time_of_day,
                                   self.settings)

    def sanitize_sides(self):
        """
        Make sure the opposing factions are using different countries
        :return:
        """
        if self.player_country == self.enemy_country:
            if self.player_country == "USA":
                self.enemy_country = "USAF Aggressors"
            elif self.player_country == "Russia":
                self.enemy_country = "USSR"
            else:
                self.enemy_country = "Russia"

    @property
    def player_faction(self) -> Faction:
        return db.FACTIONS[self.player_name]

    @property
    def enemy_faction(self) -> Faction:
        return db.FACTIONS[self.enemy_name]

    def faction_for(self, player: bool) -> Faction:
        if player:
            return self.player_faction
        return self.enemy_faction

    def _roll(self, prob, mult):
        if self.settings.version == "dev":
            # always generate all events for dev
            return 100
        else:
            return random.randint(1, 100) <= prob * mult

    def _generate_player_event(self, event_class, player_cp, enemy_cp):
        self.events.append(
            event_class(
                self,
                player_cp,
                enemy_cp,
                enemy_cp.position,
                self.player_name,
                self.enemy_name,
            ))

    def _generate_events(self):
        for front_line in self.theater.conflicts(True):
            self._generate_player_event(
                FrontlineAttackEvent,
                front_line.control_point_a,
                front_line.control_point_b,
            )

    def adjust_budget(self, amount: float, player: bool) -> None:
        if player:
            self.budget += amount
        else:
            self.enemy_budget += amount

    def process_player_income(self):
        self.budget += Income(self, player=True).total

    def process_enemy_income(self):
        # TODO: Clean up save compat.
        if not hasattr(self, "enemy_budget"):
            self.enemy_budget = 0
        self.enemy_budget += Income(self, player=False).total

    def initiate_event(self, event: Event) -> UnitMap:
        # assert event in self.events
        logging.info("Generating {} (regular)".format(event))
        return event.generate()

    def finish_event(self, event: Event, debriefing: Debriefing):
        logging.info("Finishing event {}".format(event))
        event.commit(debriefing)

        if event in self.events:
            self.events.remove(event)
        else:
            logging.info("finish_event: event not in the events!")

    def is_player_attack(self, event):
        if isinstance(event, Event):
            return (event and event.attacker_name
                    and event.attacker_name == self.player_name)
        else:
            raise RuntimeError(
                f"{event} was passed when an Event type was expected")

    def on_load(self) -> None:
        LuaPluginManager.load_settings(self.settings)
        ObjectiveDistanceCache.set_theater(self.theater)
        self.compute_conflicts_position()
        self.compute_threat_zones()

    def pass_turn(self, no_action: bool = False) -> None:
        logging.info("Pass turn")
        self.informations.append(
            Information("End of turn #" + str(self.turn), "-" * 40, 0))
        self.turn += 1

        for control_point in self.theater.controlpoints:
            control_point.process_turn(self)

        self.process_enemy_income()

        self.process_player_income()

        if not no_action and self.turn > 1:
            for cp in self.theater.player_points():
                cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
        else:
            for cp in self.theater.player_points():
                if not cp.is_carrier and not cp.is_lha:
                    cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)

        self.conditions = self.generate_conditions()

        self.initialize_turn()

        # Autosave progress
        persistency.autosave(self)

    def check_win_loss(self):
        captured_states = {i.captured for i in self.theater.controlpoints}
        if True not in captured_states:
            return TurnState.LOSS
        if False not in captured_states:
            return TurnState.WIN
        return TurnState.CONTINUE

    def initialize_turn(self) -> None:
        self.events = []
        self._generate_events()

        # Update statistics
        self.game_stats.update(self)

        self.aircraft_inventory.reset()
        for cp in self.theater.controlpoints:
            self.aircraft_inventory.set_from_control_point(cp)

        # Check for win or loss condition
        turn_state = self.check_win_loss()
        if turn_state in (TurnState.LOSS, TurnState.WIN):
            return self.process_win_loss(turn_state)

        # Plan flights & combat for next turn
        self.compute_conflicts_position()
        self.compute_threat_zones()
        self.ground_planners = {}
        self.blue_ato.clear()
        self.red_ato.clear()

        blue_planner = CoalitionMissionPlanner(self, is_player=True)
        blue_planner.plan_missions()

        red_planner = CoalitionMissionPlanner(self, is_player=False)
        red_planner.plan_missions()

        for cp in self.theater.controlpoints:
            if cp.has_frontline:
                gplanner = GroundPlanner(cp, self)
                gplanner.plan_groundwar()
                self.ground_planners[cp.id] = gplanner

        self.plan_procurement(blue_planner, red_planner)

    def plan_procurement(
        self,
        blue_planner: CoalitionMissionPlanner,
        red_planner: CoalitionMissionPlanner,
    ) -> None:
        # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
        # gets much more of the budget that turn. Otherwise budget (after
        # repairs) is split evenly between air and ground. For the default
        # starting budget of 2000 this gives 600 to ground forces and 1400 to
        # aircraft.
        ground_portion = 0.3 if self.turn == 0 else 0.5
        self.budget = ProcurementAi(
            self,
            for_player=True,
            faction=self.player_faction,
            manage_runways=self.settings.automate_runway_repair,
            manage_front_line=self.settings.automate_front_line_reinforcements,
            manage_aircraft=self.settings.automate_aircraft_reinforcements,
            front_line_budget_share=ground_portion,
        ).spend_budget(self.budget, blue_planner.procurement_requests)

        self.enemy_budget = ProcurementAi(
            self,
            for_player=False,
            faction=self.enemy_faction,
            manage_runways=True,
            manage_front_line=True,
            manage_aircraft=True,
            front_line_budget_share=ground_portion,
        ).spend_budget(self.enemy_budget, red_planner.procurement_requests)

    def message(self, text: str) -> None:
        self.informations.append(Information(text, turn=self.turn))

    @property
    def current_turn_time_of_day(self) -> TimeOfDay:
        return list(TimeOfDay)[self.turn % 4]

    @property
    def current_day(self) -> date:
        return self.date + timedelta(days=self.turn // 4)

    def next_unit_id(self):
        """
        Next unit id for pre-generated units
        """
        self.current_unit_id += 1
        return self.current_unit_id

    def next_group_id(self):
        """
        Next unit id for pre-generated units
        """
        self.current_group_id += 1
        return self.current_group_id

    def compute_threat_zones(self) -> None:
        self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
        self.red_threat_zone = ThreatZones.for_faction(self, player=False)
        self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
                                                      self.theater)
        self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
                                                     self.theater)

    def threat_zone_for(self, player: bool) -> ThreatZones:
        if player:
            return self.blue_threat_zone
        return self.red_threat_zone

    def navmesh_for(self, player: bool) -> NavMesh:
        if player:
            return self.blue_navmesh
        return self.red_navmesh

    def compute_conflicts_position(self):
        """
        Compute the current conflict center position(s), mainly used for culling calculation
        :return: List of points of interests
        """
        zones = []
        points = []

        # By default, use the existing frontline conflict position
        for front_line in self.theater.conflicts():
            position = Conflict.frontline_position(front_line.control_point_a,
                                                   front_line.control_point_b,
                                                   self.theater)
            zones.append(position[0])
            zones.append(front_line.control_point_a.position)
            zones.append(front_line.control_point_b.position)

        for cp in self.theater.controlpoints:
            # Don't cull missile sites - their range is long enough to make them
            # easily culled despite being a threat.
            for tgo in cp.ground_objects:
                if isinstance(tgo, MissileSiteGroundObject):
                    points.append(tgo.position)
            # If do_not_cull_carrier is enabled, add carriers as culling point
            if self.settings.perf_do_not_cull_carrier:
                if cp.is_carrier or cp.is_lha:
                    zones.append(cp.position)

        # If there is no conflict take the center point between the two nearest opposing bases
        if len(zones) == 0:
            cpoint = None
            min_distance = sys.maxsize
            for cp in self.theater.player_points():
                for cp2 in self.theater.enemy_points():
                    d = cp.position.distance_to_point(cp2.position)
                    if d < min_distance:
                        min_distance = d
                        cpoint = Point(
                            (cp.position.x + cp2.position.x) / 2,
                            (cp.position.y + cp2.position.y) / 2,
                        )
                        zones.append(cp.position)
                        zones.append(cp2.position)
                        break
                if cpoint is not None:
                    break
            if cpoint is not None:
                zones.append(cpoint)

        packages = itertools.chain(self.blue_ato.packages,
                                   self.red_ato.packages)
        for package in packages:
            if package.primary_task is FlightType.BARCAP:
                # BARCAPs will be planned at most locations on smaller theaters,
                # rendering culling fairly useless. BARCAP packages don't really
                # need the ground detail since they're defensive. SAMs nearby
                # are only interesting if there are enemies in the area, and if
                # there are they won't be culled because of the enemy's mission.
                continue
            zones.append(package.target.position)

        # Else 0,0, since we need a default value
        # (in this case this means the whole map is owned by the same player, so it is not an issue)
        if len(zones) == 0:
            zones.append(Point(0, 0))

        self.__culling_zones = zones
        self.__culling_points = points

    def add_destroyed_units(self, data):
        pos = Point(data["x"], data["z"])
        if self.theater.is_on_land(pos):
            self.__destroyed_units.append(data)

    def get_destroyed_units(self):
        return self.__destroyed_units

    def position_culled(self, pos):
        """
        Check if unit can be generated at given position depending on culling performance settings
        :param pos: Position you are tryng to spawn stuff at
        :return: True if units can not be added at given position
        """
        if self.settings.perf_culling == False:
            return False
        else:
            for z in self.__culling_zones:
                if (z.distance_to_point(pos) <
                        self.settings.perf_culling_distance * 1000):
                    return False
            for p in self.__culling_points:
                if p.distance_to_point(pos) < 2500:
                    return False
            return True

    def get_culling_zones(self):
        """
        Check culling points
        :return: List of culling zones
        """
        return self.__culling_zones

    def get_culling_points(self):
        """
        Check culling points
        :return: List of culling points
        """
        return self.__culling_points

    # 1 = red, 2 = blue
    def get_player_coalition_id(self):
        return 2

    def get_enemy_coalition_id(self):
        return 1

    def get_player_coalition(self):
        return Coalition.Blue

    def get_enemy_coalition(self):
        return Coalition.Red

    def get_player_color(self):
        return "blue"

    def get_enemy_color(self):
        return "red"

    def process_win_loss(self, turn_state: TurnState):
        if turn_state is TurnState.WIN:
            return self.message(
                "Congratulations, you are victorious!  Start a new campaign to continue."
            )
        elif turn_state is TurnState.LOSS:
            return self.message(
                "Game Over, you lose. Start a new campaign to continue.")
Пример #3
0
class Game:
    def __init__(self, player_name: str, enemy_name: str,
                 theater: ConflictTheater, start_date: datetime,
                 settings: Settings):
        self.settings = settings
        self.events: List[Event] = []
        self.theater = theater
        self.player_name = player_name
        self.player_country = db.FACTIONS[player_name].country
        self.enemy_name = enemy_name
        self.enemy_country = db.FACTIONS[enemy_name].country
        self.turn = 0
        self.date = date(start_date.year, start_date.month, start_date.day)
        self.game_stats = GameStats()
        self.game_stats.update(self)
        self.ground_planners: Dict[int, GroundPlanner] = {}
        self.informations = []
        self.informations.append(Information("Game Start", "-" * 40, 0))
        self.__culling_points = self.compute_conflicts_position()
        self.__destroyed_units: List[str] = []
        self.savepath = ""
        self.budget = PLAYER_BUDGET_INITIAL
        self.current_unit_id = 0
        self.current_group_id = 0

        self.conditions = self.generate_conditions()

        self.blue_ato = AirTaskingOrder()
        self.red_ato = AirTaskingOrder()

        self.aircraft_inventory = GlobalAircraftInventory(
            self.theater.controlpoints)

        self.sanitize_sides()
        self.on_load()

    def generate_conditions(self) -> Conditions:
        return Conditions.generate(self.theater, self.date,
                                   self.current_turn_time_of_day,
                                   self.settings)

    def sanitize_sides(self):
        """
        Make sure the opposing factions are using different countries
        :return:
        """
        if self.player_country == self.enemy_country:
            if self.player_country == "USA":
                self.enemy_country = "USAF Aggressors"
            elif self.player_country == "Russia":
                self.enemy_country = "USSR"
            else:
                self.enemy_country = "Russia"

    @property
    def player_faction(self) -> Faction:
        return db.FACTIONS[self.player_name]

    @property
    def enemy_faction(self) -> Faction:
        return db.FACTIONS[self.enemy_name]

    def _roll(self, prob, mult):
        if self.settings.version == "dev":
            # always generate all events for dev
            return 100
        else:
            return random.randint(1, 100) <= prob * mult

    def _generate_player_event(self, event_class, player_cp, enemy_cp):
        self.events.append(
            event_class(self, player_cp, enemy_cp, enemy_cp.position,
                        self.player_name, self.enemy_name))

    def _generate_events(self):
        for front_line in self.theater.conflicts(True):
            self._generate_player_event(FrontlineAttackEvent,
                                        front_line.control_point_a,
                                        front_line.control_point_b)

    @property
    def budget_reward_amount(self):
        reward = 0
        if len(self.theater.player_points()) > 0:
            reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
            for cp in self.theater.player_points():
                for g in cp.ground_objects:
                    if g.category in REWARDS.keys():
                        reward = reward + REWARDS[g.category]
            return reward
        else:
            return reward

    def _budget_player(self):
        self.budget += self.budget_reward_amount

    def awacs_expense_commit(self):
        self.budget -= AWACS_BUDGET_COST

    def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
        event = UnitsDeliveryEvent(attacker_name=self.player_name,
                                   defender_name=self.player_name,
                                   from_cp=to_cp,
                                   to_cp=to_cp,
                                   game=self)
        self.events.append(event)
        return event

    def units_delivery_remove(self, event: Event):
        if event in self.events:
            self.events.remove(event)

    def initiate_event(self, event: Event):
        #assert event in self.events
        logging.info("Generating {} (regular)".format(event))
        event.generate()

    def finish_event(self, event: Event, debriefing: Debriefing):
        logging.info("Finishing event {}".format(event))
        event.commit(debriefing)
        if event.is_successfull(debriefing):
            self.budget += event.bonus()

        if event in self.events:
            self.events.remove(event)
        else:
            logging.info("finish_event: event not in the events!")

    def is_player_attack(self, event):
        if isinstance(event, Event):
            return event and event.attacker_name and event.attacker_name == self.player_name
        else:
            return event and event.name and event.name == self.player_name

    def on_load(self) -> None:
        LuaPluginManager.load_settings(self.settings)
        ObjectiveDistanceCache.set_theater(self.theater)

        # Save game compatibility.

        # TODO: Remove in 2.3.
        if not hasattr(self, "conditions"):
            self.conditions = self.generate_conditions()

    def pass_turn(self, no_action: bool = False) -> None:
        logging.info("Pass turn")
        self.informations.append(
            Information("End of turn #" + str(self.turn), "-" * 40, 0))
        self.turn += 1

        for event in self.events:
            if self.settings.version == "dev":
                # don't damage player CPs in by skipping in dev mode
                if isinstance(event, UnitsDeliveryEvent):
                    event.skip()
            else:
                event.skip()

        self._enemy_reinforcement()
        self._budget_player()

        if not no_action and self.turn > 1:
            for cp in self.theater.player_points():
                cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
        else:
            for cp in self.theater.player_points():
                if not cp.is_carrier and not cp.is_lha:
                    cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)

        self.conditions = self.generate_conditions()

        self.initialize_turn()

        # Autosave progress
        persistency.autosave(self)

    def initialize_turn(self) -> None:
        self.events = []
        self._generate_events()

        # Update statistics
        self.game_stats.update(self)

        self.aircraft_inventory.reset()
        for cp in self.theater.controlpoints:
            self.aircraft_inventory.set_from_control_point(cp)

        # Plan flights & combat for next turn
        self.__culling_points = self.compute_conflicts_position()
        self.ground_planners = {}
        self.blue_ato.clear()
        self.red_ato.clear()
        CoalitionMissionPlanner(self, is_player=True).plan_missions()
        CoalitionMissionPlanner(self, is_player=False).plan_missions()
        for cp in self.theater.controlpoints:
            if cp.has_frontline:
                gplanner = GroundPlanner(cp, self)
                gplanner.plan_groundwar()
                self.ground_planners[cp.id] = gplanner

    def _enemy_reinforcement(self):
        """
        Compute and commision reinforcement for enemy bases
        """

        MAX_ARMOR = 30 * self.settings.multiplier
        MAX_AIRCRAFT = 25 * self.settings.multiplier

        production = 0.0
        for enemy_point in self.theater.enemy_points():
            for g in enemy_point.ground_objects:
                if g.category in REWARDS.keys():
                    production = production + REWARDS[g.category]

        production = production * 0.75
        budget_for_armored_units = production / 2
        budget_for_aircraft = production / 2

        potential_cp_armor = []
        for cp in self.theater.enemy_points():
            for cpe in cp.connected_points:
                if cpe.captured and cp.base.total_armor < MAX_ARMOR:
                    potential_cp_armor.append(cp)
        if len(potential_cp_armor) == 0:
            potential_cp_armor = self.theater.enemy_points()

        i = 0
        potential_units = db.FACTIONS[self.enemy_name].frontline_units

        print("Enemy Recruiting")
        print(potential_cp_armor)
        print(budget_for_armored_units)
        print(potential_units)

        if len(potential_units) > 0 and len(potential_cp_armor) > 0:
            while budget_for_armored_units > 0:
                i = i + 1
                if i > 50 or budget_for_armored_units <= 0:
                    break
                target_cp = random.choice(potential_cp_armor)
                if target_cp.base.total_armor >= MAX_ARMOR:
                    continue
                unit = random.choice(potential_units)
                price = db.PRICES[unit] * 2
                budget_for_armored_units -= price * 2
                target_cp.base.armor[unit] = target_cp.base.armor.get(unit,
                                                                      0) + 2
                info = Information("Enemy Reinforcement",
                                   unit.id + " x 2 at " + target_cp.name,
                                   self.turn)
                print(str(info))
                self.informations.append(info)

        if budget_for_armored_units > 0:
            budget_for_aircraft += budget_for_armored_units

        potential_units = [
            u for u in db.FACTIONS[self.enemy_name].aircrafts
            if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]
        ]

        if len(potential_units) > 0 and len(potential_cp_armor) > 0:
            while budget_for_aircraft > 0:
                i = i + 1
                if i > 50 or budget_for_aircraft <= 0:
                    break
                target_cp = random.choice(potential_cp_armor)
                if target_cp.base.total_planes >= MAX_AIRCRAFT:
                    continue
                unit = random.choice(potential_units)
                price = db.PRICES[unit] * 2
                budget_for_aircraft -= price * 2
                target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(
                    unit, 0) + 2
                info = Information("Enemy Reinforcement",
                                   unit.id + " x 2 at " + target_cp.name,
                                   self.turn)
                print(str(info))
                self.informations.append(info)

    @property
    def current_turn_time_of_day(self) -> TimeOfDay:
        return list(TimeOfDay)[self.turn % 4]

    @property
    def current_day(self) -> date:
        return self.date + timedelta(days=self.turn // 4)

    def next_unit_id(self):
        """
        Next unit id for pre-generated units
        """
        self.current_unit_id += 1
        return self.current_unit_id

    def next_group_id(self):
        """
        Next unit id for pre-generated units
        """
        self.current_group_id += 1
        return self.current_group_id

    def compute_conflicts_position(self):
        """
        Compute the current conflict center position(s), mainly used for culling calculation
        :return: List of points of interests
        """
        points = []

        # By default, use the existing frontline conflict position
        for front_line in self.theater.conflicts():
            position = Conflict.frontline_position(self.theater,
                                                   front_line.control_point_a,
                                                   front_line.control_point_b)
            points.append(position[0])
            points.append(front_line.control_point_a.position)
            points.append(front_line.control_point_b.position)

        # If there is no conflict take the center point between the two nearest opposing bases
        if len(points) == 0:
            cpoint = None
            min_distance = sys.maxsize
            for cp in self.theater.player_points():
                for cp2 in self.theater.enemy_points():
                    d = cp.position.distance_to_point(cp2.position)
                    if d < min_distance:
                        min_distance = d
                        cpoint = Point((cp.position.x + cp2.position.x) / 2,
                                       (cp.position.y + cp2.position.y) / 2)
                        points.append(cp.position)
                        points.append(cp2.position)
                        break
                if cpoint is not None:
                    break
            if cpoint is not None:
                points.append(cpoint)

        # Else 0,0, since we need a default value
        # (in this case this means the whole map is owned by the same player, so it is not an issue)
        if len(points) == 0:
            points.append(Point(0, 0))

        return points

    def add_destroyed_units(self, data):
        pos = Point(data["x"], data["z"])
        if self.theater.is_on_land(pos):
            self.__destroyed_units.append(data)

    def get_destroyed_units(self):
        return self.__destroyed_units

    def position_culled(self, pos):
        """
        Check if unit can be generated at given position depending on culling performance settings
        :param pos: Position you are tryng to spawn stuff at
        :return: True if units can not be added at given position
        """
        if self.settings.perf_culling == False:
            return False
        else:
            for c in self.__culling_points:
                if c.distance_to_point(
                        pos) < self.settings.perf_culling_distance * 1000:
                    return False
            return True

    def get_culling_points(self):
        """
        Check culling points
        :return: List of culling points
        """
        return self.__culling_points

    # 1 = red, 2 = blue
    def get_player_coalition_id(self):
        return 2

    def get_enemy_coalition_id(self):
        return 1

    def get_player_coalition(self):
        return Coalition.Blue

    def get_enemy_coalition(self):
        return Coalition.Red

    def get_player_color(self):
        return "blue"

    def get_enemy_color(self):
        return "red"