Jump to content

FE8 auction #6: Partial Lagdou


JudgeWargrave
 Share

Recommended Posts

FE8 is definitely the shortest GBA game, but it also has the most postgame content. I thought it would be interesting to include part of the Lagdou Ruins to freshen things up. Since it's an experiment and ten chapters of routing monsters might get repetitive I thought we should just try the first five floors after defeating the Demon King. (Then again FE6 endless seize objectives are more repetitive anyway). This will make late game units a little bit better and will let other units have more time to develop and contribute than they would in the faster paced format that ends at the Demon King. It might also lead to interesting decisions regarding Warp conservation since you will want it for five additional chapters.

There are really only 31 units, but for the blank 8th slot on one of the teams you can use the postgame units, although in this format the only one you may be able to unlock is Hayden, not sure if there are 200 enemies you can kill before the fifth floor. I also moved Seth one chapter earlier, FE8 doesn't front-load its roster as much as FE6/7 so I wanted to help the early game be a little bit easier.

Team structure:
1. There will be four players. There are 32 units to draft.    
2. All teams automatically include:    
    a. The main lord, (Ephraim counts as the main lord for 5x)
    b. Tethys,
    c. Orson,
    d. The secondary lord for 8 and 15.
3. Seth may only be used starting chapter 5.
4. The player with postgame units may use any they unlock.    
    
Rules:    
1. Play on Hard Mode. The game ends after beating the 5th floor of Lagdou Ruins. Defeat the Demon King before entering the Ruins    
2. Restricted actions:
    a. You may not voluntarily deploy undrafted units except on chapters with talk recruitments, specifically 5, 9, 10, 11, 12b, 13a, 14, and 17.
    b. Undrafted units may move, recruit units, rescue/drop undrafted units, trade between chapters, and dig up items in the desert.
    c. Undrafted units may not do anything else, including but not limited to: enter combat, use items, take items or have them put in their inventory, visit (any building), rescue/drop drafted units or NPCs, steal, pick, or have a support rank.
    d. Drafted units may do as you please without penalty.
    e. Do not enter avoidable skirmishes, the Tower, or Ruins before defeating the Demon King. You must immediately retreat from unavoidable skirmishes.
    f. Mine/enemy control glitch is banned
    g. Using extra swiftsoles is banned
3. Defend chapters count the last played Player Phase for turns if the timer is waited out.    
4. Map shopping is allowed, including postgame secret shops.
    
Penalties:    
1. You may use an undrafted unit as a drafted unit with a 4 turn penalty, per unit per chapter. There is no special penalty for Seth.

 

For the auction format, I've made it simpler, arguably. The automatic team value adjustments for promo items and chapterwise redundancy are removed, though the redundancy function is still applied to the teams as a whole. Instead you can set synergy values for pairs of units manually. For example, you might set a -2 value between Franz and Gilliam, since one of them would need to delay their promotion so their value is lower than it would be otherwise. This lets people cover not only promo item and chapterwise redundancy, but redundant team roles (e.g. flier, warper), route split considerations (e.g. Cormag goes poorly with Innes and Saleh), and pretty much anything you can think of. You can PM me your synergy values with your bids formatted like they are in the sample auction below. If you want you can also choose to set all your synergy values to the median of the values people did submit; in that case just submit bids.

By the way, my bids/synergy values are the ones used in the sample, not bothering with mocking up different values and hashing mine to prove they aren't changed. Feel free to try over/underbidding me.

Example auction:

Spoiler

Reading synergy values from FE8auction3A.syn.txt.
  Synergies for -A-       
Franz       :  Gilliam     -1.50  Vanessa     -1.50  Seth>=5     -1.00  Forde       -1.50  Kyle        -1.50 
Gilliam     :  Vanessa     -1.00  Ross        -0.50  Garcia      -0.50  Forde       -1.50  Kyle        -1.50 
Vanessa     :  Seth>=5     -1.00  Tana        -3.00  Cormag      -4.00  Syrene      -1.50 
Moulder     :  Artur       -2.00  Lute        -2.00  Natasha     -2.50  Saleh       -1.00 
Ross        :  Garcia      -0.50  Colm        -1.00  Dozla       -0.50 
Garcia      :  Joshua      -1.00  Gerik       -1.00  Dozla       -0.50 
Neimi       :  Innes       -1.00 
Colm        :  Joshua      -1.00  Marisa      -0.50  Rennac      -0.50 
Artur       :  Lute        -2.00  Natasha     -2.00  Saleh       -1.00 
Lute        :  Natasha     -2.00  Saleh       -1.00 
Seth>=5     :  Forde       -0.50  Kyle        -0.50 
Natasha     :  Saleh       -1.00 
Joshua      :  Gerik       -1.00  Marisa      -1.00  Rennac      -0.50 
Forde       :  Kyle        -0.50 
Tana        :  Cormag      -5.00  Duessel     -0.50  Syrene      -1.50 
Gerik       :  Marisa      -1.00  Cormag      -1.00  Duessel     -0.50 
Innes       :  Marisa      -0.80  Cormag      -0.50  Duessel     -0.50 
Marisa      :  Saleh       -0.80  Cormag       1.00  Rennac      -0.50  Duessel      0.50  2nd Lord    -0.50 
Saleh       :  Cormag      -1.00  Duessel     -1.00 
Cormag      :  Duessel      2.00  2nd Lord    -0.50  Syrene      -1.00 
Duessel     :  2nd Lord    -0.50 
--BIDS--        -A-        -B-        -C-        -D-        
Franz           13.50      13.50      13.50      13.50     
Gilliam          7.50       7.50       7.50       7.50     
Vanessa         16.00      16.00      16.00      16.00     
Moulder          7.00       7.00       7.00       7.00     
Ross             5.00       5.00       5.00       5.00     
Garcia           6.50       6.50       6.50       6.50     
Neimi            4.00       4.00       4.00       4.00     
Colm             4.00       4.00       4.00       4.00     
Artur            9.50       9.50       9.50       9.50     
Lute             8.50       8.50       8.50       8.50     
Seth>=5         14.00      14.00      14.00      14.00     
Natasha          5.00       5.00       5.00       5.00     
Joshua           3.00       3.00       3.00       3.00     
Forde            8.50       8.50       8.50       8.50     
Kyle             8.50       8.50       8.50       8.50     
Tana            10.00      10.00      10.00      10.00     
Amelia           2.00       2.00       2.00       2.00     
Gerik            3.00       3.00       3.00       3.00     
Innes            1.50       1.50       1.50       1.50     
Marisa           0.00       0.00       0.00       0.00     
Dozla            2.50       2.50       2.50       2.50     
L'Arachel        2.50       2.50       2.50       2.50     
Saleh            4.50       4.50       4.50       4.50     
Ewan             1.20       1.20       1.20       1.20     
Cormag           8.00       8.00       8.00       8.00     
Rennac           1.00       1.00       1.00       1.00     
Duessel          3.00       3.00       3.00       3.00     
Knoll            1.50       1.50       1.50       1.50     
2nd Lord         2.00       2.00       2.00       2.00     
Myrrh            1.50       1.50       1.50       1.50     
Syrene           3.00       3.00       3.00       3.00     
Bonus Units      0.00       0.00       0.00       0.00     

---Initial assignments---
Franz        to 0 -A-         
Gilliam      to 0 -A-         
Vanessa      to 0 -A-         
Moulder      to 0 -A-         
Ross         to 0 -A-         
Garcia       to 0 -A-         
Neimi        to 0 -A-         
Colm         to 0 -A-         
Artur        to 1 -B-         
Lute         to 1 -B-         
Seth>=5      to 1 -B-         
Natasha      to 1 -B-         
Joshua       to 1 -B-         
Forde        to 1 -B-         
Kyle         to 1 -B-         
Tana         to 1 -B-         
Amelia       to 2 -C-         
Gerik        to 2 -C-         
Innes        to 2 -C-         
Marisa       to 2 -C-         
Dozla        to 2 -C-         
L'Arachel    to 2 -C-         
Saleh        to 2 -C-         
Ewan         to 2 -C-         
Cormag       to 3 -D-         
Rennac       to 3 -D-         
Duessel      to 3 -D-         
Knoll        to 3 -D-         
2nd Lord     to 3 -D-         
Myrrh        to 3 -D-         
Syrene       to 3 -D-         
Bonus Units  to 3 -D-         
Swapping -A-          Franz        <-> Artur        -B-         , new score  36.771
Swapping -B-          Franz        <-> Amelia       -C-         , new score  38.466
Swapping -C-          Franz        <-> Syrene       -D-         , new score  38.532
Swapping -A-          Gilliam      <-> Forde        -B-         , new score  38.772
Swapping -B-          Gilliam      <-> Gerik        -C-         , new score  39.505
Swapping -A-          Vanessa      <-> Innes        -C-         , new score  39.710
Swapping -A-          Moulder      <-> Marisa       -C-         , new score  39.825
Swapping -C-          Moulder      <-> Rennac       -D-         , new score  40.077
Swapping -A-          Ross         <-> Lute         -B-         , new score  40.419
Swapping -B-          Ross         <-> Innes        -A-         , new score  40.531
Swapping -A-          Ross         <-> Dozla        -C-         , new score  40.647
Swapping -C-          Ross         <-> Knoll        -D-         , new score  40.722
Swapping -D-          Ross         <-> Syrene       -C-         , new score  40.746
Swapping -C-          Ross         <-> Bonus Units  -D-         , new score  40.786
Swapping -A-          Garcia       <-> L'Arachel    -C-         , new score  40.819
Swapping -C-          Garcia       <-> 2nd Lord     -D-         , new score  40.899
Swapping -D-          Garcia       <-> Bonus Units  -C-         , new score  41.027
Swapping -A-          Neimi        <-> Cormag       -D-         , new score  41.027
Swapping -A-          Colm         <-> Joshua       -B-         , new score  41.099
Swapping -B-          Colm         <-> Rennac       -C-         , new score  41.137
Swapping -A-          Artur        <-> Kyle         -B-         , new score  41.141
Swapping -B-          Artur        <-> Saleh        -C-         , new score  41.305
Swapping -A-          Lute         <-> Duessel      -D-         , new score  41.407
Swapping -B-          Natasha      <-> Joshua       -A-         , new score  41.499
Swapping -B-          Joshua       <-> Forde        -A-         , new score  41.556
Swapping -A-          Joshua       <-> Myrrh        -D-         , new score  41.737
Swapping -B-          Forde        <-> Dozla        -A-         , new score  41.789
Swapping -B-          Amelia       <-> L'Arachel    -A-         , new score  41.790
Swapping -B-          Innes        <-> 2nd Lord     -C-         , new score  41.798
Swapping -C-          Ewan         <-> Bonus Units  -D-         , new score  41.806
Swapping -C-          Gilliam      <-> Forde        -A-         , new score  41.860
Swapping -A-          Gilliam      <-> Dozla        -B-         , new score  42.090
Swapping -D-          Moulder      <-> Artur        -C-         , new score  42.099
Swapping -C-          Moulder      <-> Natasha      -A-         , new score  42.141
Swapping -D-          Ross         <-> Dozla        -A-         , new score  42.182
Swapping -D-          Neimi        <-> Amelia       -A-         , new score  42.195
Swapping -D-          Artur        <-> L'Arachel    -B-         , new score  42.274
Swapping -B-          Tana         <-> Syrene       -D-         , new score  42.386
Swapping -D-          Amelia       <-> Myrrh        -A-         , new score  42.388
Swapping -B-          2nd Lord     <-> Myrrh        -D-         , new score  42.388
Swapping -A-          Moulder      <-> Lute         -D-         , new score  42.391
Swapping -A-          Amelia       <-> Ewan         -D-         , new score  42.391
Swapping -A-          Ewan         <-> Knoll        -C-         , new score  42.391
   0.00   0/ 14  Rotation  (0, 2, 3, 1)   Trading players  [1, 2, 3]
   0.17   1/ 14  Rotation  (0, 3, 1, 2)   Trading players  [1, 2, 3]
   0.32   2/ 14  Rotation  (1, 2, 0, 3)   Trading players  [0, 1, 2]
   0.49   3/ 14  Rotation  (1, 2, 3, 0)   Trading players  [0, 1, 2, 3]
   1.78   4/ 14  Rotation  (1, 3, 0, 2)   Trading players  [0, 1, 2, 3]
   3.06   5/ 14  Rotation  (1, 3, 2, 0)   Trading players  [0, 1, 3]
   3.19   6/ 14  Rotation  (2, 0, 1, 3)   Trading players  [0, 1, 2]
   3.36   7/ 14  Rotation  (2, 0, 3, 1)   Trading players  [0, 1, 2, 3]
   4.57   8/ 14  Rotation  (2, 1, 3, 0)   Trading players  [0, 2, 3]
   4.73   9/ 14  Rotation  (2, 3, 1, 0)   Trading players  [0, 1, 2, 3]
   5.94  10/ 14  Rotation  (3, 0, 1, 2)   Trading players  [0, 1, 2, 3]
   7.11  11/ 14  Rotation  (3, 0, 2, 1)   Trading players  [0, 1, 3]
   7.27  12/ 14  Rotation  (3, 1, 0, 2)   Trading players  [0, 2, 3]
   7.45  13/ 14  Rotation  (3, 2, 0, 1)   Trading players  [0, 1, 2, 3]

           -A-        -B-        -C-        -D-        Comparative satisfaction

Unadjusted value matrix
-A-          38.50      44.00      42.70      42.50      -4.57
-B-          38.50      44.00      42.70      42.50       2.77
-C-          38.50      44.00      42.70      42.50       1.03
-D-          38.50      44.00      42.70      42.50       0.77

Synergy
-A-           3.50      -1.00       0.00       0.00       3.83
-B-           3.50      -1.00       0.00       0.00      -2.17
-C-           3.50      -1.00       0.00       0.00      -0.83
-D-           3.50      -1.00       0.00       0.00      -0.83

Synergy adjusted
-A-          42.00      43.00      42.70      42.50      -0.73
-B-          42.00      43.00      42.70      42.50       0.60
-C-          42.00      43.00      42.70      42.50       0.20
-D-          42.00      43.00      42.70      42.50      -0.07

Redundancy adjusted
-A-          41.98      42.73      42.50      42.35      -0.55
-B-          41.98      42.73      42.50      42.35       0.45
-C-          41.98      42.73      42.50      42.35       0.15
-D-          41.98      42.73      42.50      42.35      -0.05

Average team robustness:  42.39
HANDICAPS:    0.00       0.74       0.52       0.37    

Handicap adjusted
-A-          41.98      41.98      41.98      41.98       0.00
-B-          41.98      41.98      41.98      41.98       0.00
-C-          41.98      41.98      41.98      41.98       0.00
-D-          41.98      41.98      41.98      41.98       0.00

---Teams---
-A-          -B-          -C-          -D-          
Ross         Gilliam      Vanessa      Franz        
Neimi        Artur        Garcia       Moulder      
Lute         Seth>=5      Colm         Joshua       
Kyle         Gerik        Natasha      Tana         
Marisa       Saleh        Forde        Amelia       
Cormag       Rennac       Innes        Dozla        
Duessel      Myrrh        Ewan         L'Arachel    
Knoll        Syrene       Bonus Units  2nd Lord     
 0.00         0.74         0.52         0.37        

 

For some reason uploading the Python files that run the auction isn't working right now, I'll try again in a bit, if need be I guess I'll paste them in or upload them somewhere else and post a link.

Edit: yeah I keep getting an error with -200 and no other message when trying to attach these.

Main.py

Spoiler

import AuctionState
import cProfile

test = AuctionState.AuctionState(['-A-', '-B-', '-C-', '-D-'])
test.robust_factor = 0.25

def main():
    test.read_bids('FE8auction3.bids.txt')

    test.read_synergy('FE8auction3A.syn.txt')
    test.set_median_synergy()
    test.set_median_synergy()
    test.set_median_synergy()
    test.print_synergy(0)
    test.run()


# cProfile.run('main()')
main()

 

AuctionState.py

Spoiler

import GameData
import Pricing
import itertools
import time
import statistics
import random


def extend_array(array, length, filler):
    while len(array) < length:
        array.append(filler)


class AuctionState:
    def __init__(self, players):
        self.players = players
        self.max_team_size = len(GameData.units)//len(players)
        self.opp_ratio = 1 - 1/len(players)  # used in pricing functions
        self.team_sizes = []  # index -1 for unassigned

        self.robust_factor = 0.25  # bias in favor of good teams rather than expected victory
        # index by player last for printing/reading and for consistency
        self.bids = []  # U x P, indexing for reading files/printing
        self.bid_sums = [0] * len(players)  # P, used for redundancy adjusted team values
        self.MC_matrix = []  # C x P, M values by chapter
        self.unit_value_per_chapter = []  # U x P, unmodified by promo items

        self.synergies = []
        # P x U x U, players choose value to reduce/increase value of unit pairs
        # generally negative for redundant units
        # increment value of team by manual_synergy[i][j][valuer] if i and j on same team
        # should be triangular matrix since synergy i<->j == j<->i

        self.synergy_relationship_graph = [set() for i in range(len(GameData.units))]

        self.rotations = [p for p in itertools.permutations(range(len(players))) if Pricing.just_one_loop(p)]

    def read_bids(self, bid_file_name):
        try:
            self.bid_sums = [0] * len(self.players)
            self.bids = []

            bid_file = open(bid_file_name, 'r')
            for line in bid_file.readlines():
                next_bid_row = [float(i) for i in line.split()]

                if len(next_bid_row) > 0:  # skip empty lines
                    # if fewer than max players, create dummy players from existing bids
                    while len(next_bid_row) < len(self.players):
                        next_bid_row.append(statistics.median(next_bid_row) * random.triangular(1, 1))

                    self.bids.append(next_bid_row)

                    for i, bid in enumerate(next_bid_row):
                        self.bid_sums[i] += bid

            bid_file.close()
            extend_array(self.bids, len(GameData.units), [0] * len(self.players))

        except ValueError as error:
            print(error)
        except FileNotFoundError as error:
            print(error)

    def print_bids(self):
        print('--BIDS--       ', end=' ')
        for player in self.players:
            print(f'{player:10s}', end=' ')
        print()

        for bid_row, unit in zip(self.bids, GameData.units):
            unit_line = f'{unit.name:15s}'
            for bid in bid_row:
                unit_line += f' {bid:5.2f}     '
            print(unit_line)

    def read_synergy(self, synergy_file_name):
        try:
            print(f'Reading synergy values from {synergy_file_name:s}.')
            synergy_file = open(synergy_file_name, 'r')
            player_synergies = []
            for line in synergy_file.readlines():
                next_line = [float(i) for i in line.split()]
                extend_array(next_line, len(GameData.units), 0)
                player_synergies.append(next_line)

            extend_array(player_synergies, len(GameData.units), [0] * len(GameData.units))

            for u_i, synergy_row in enumerate(player_synergies):
                for u_j, syn in enumerate(synergy_row):
                    if syn != 0:
                        self.synergy_relationship_graph[u_i].add(u_j)
                        self.synergy_relationship_graph[u_j].add(u_i)

            self.synergies.append(player_synergies)
            synergy_file.close()

        except ValueError as error:
            print(error)
        except FileNotFoundError as error:
            print(error)

    def set_median_synergy(self):
        player_synergies = []
        for u_i in range(len(GameData.units)):
            next_synergy_row = []
            for u_j in range(len(GameData.units)):
                next_synergy_row.append(statistics.median([synergy[u_i][u_j] for synergy in self.synergies]))
            player_synergies.append(next_synergy_row)
        self.synergies.append(player_synergies)

    # checks that populated section of the matrix is triangular
    def print_synergy(self, player_i):
        print(f'  Synergies for {self.players[player_i]:10s}')

        for u_i in range(len(GameData.units)):
            something_to_print = False
            unit_line = f'{GameData.units[u_i].name:12s}: '
            for u_j, synergy in enumerate(self.synergies[player_i][u_i]):
                if synergy != 0:
                    something_to_print = True
                    unit_line += f' {GameData.units[u_j].name:12s}{synergy:5.2f} '
                    if u_j <= u_i:
                        print('NOTE, synergy matrix not triangular, possible error')
            if something_to_print:
                print(unit_line)

    # C x P matrix of each player's max team value on a chapter basis.
    # Divide each unit's value (bid) evenly across each chapter it is present.
    # Account for promo item competition reducing values of late promoters.
    def create_max_chapter_values(self):
        self.MC_matrix = [[0] * len(self.players) for chapter in GameData.chapters]

        self.unit_value_per_chapter = []
        for u, unit in enumerate(GameData.units):
            self.unit_value_per_chapter.append([])
            for p in range(len(self.players)):
                uvpc = self.bids[u][p] / (len(GameData.chapters) - unit.join_chapter)
                self.unit_value_per_chapter[u].append(uvpc)
                for c in range(unit.join_chapter, len(GameData.chapters)):
                    if unit.late_promo_factors:
                        # assume max competition when creating max values
                        self.MC_matrix[c][p] += uvpc * unit.late_promo_factors[-1][c]
                    else:
                        self.MC_matrix[c][p] += uvpc

    def print_max_chapter_values(self):
        print('\n---Max values by chapter---')
        for player in self.players:
            print(f'{player:10s}', end=' ')
        print()

        for row, chapter in zip(self.MC_matrix, GameData.chapters):
            for m in row:
                print(f' {m:6.2f}   ', end=' ')
            print(chapter)

    def clear_assign(self):
        self.team_sizes = [0] * len(self.players)
        self.team_sizes.append(len(GameData.units))  # all unassigned (team -1)

        for unit in GameData.units:
            unit.owner = -1

    # Only need to track team size during initial assignment;
    # afterward all assignment changes maintain team sizes, using blank slots if necessary.
    def assign_unit(self, unit, new_owner):
        unit.set_owner(new_owner)
        self.team_sizes[unit.prior_owner] -= 1
        self.team_sizes[unit.owner] += 1

    def quick_assign(self):
        self.clear_assign()
        for bid_row, unit in zip(self.bids, GameData.units):
            max_bid = -1
            for p, bid in enumerate(bid_row):
                if self.team_sizes[p] < self.max_team_size and max_bid < bid:
                    max_bid = bid
                    self.assign_unit(unit, p)

    # assign units in order of satisfaction, not recruitment
    def max_sat_assign(self):
        print()
        print('---Initial assignments---')
        self.clear_assign()
        while self.team_sizes[-1] > 0:  # unassigned units remain
            max_sat = -999
            max_sat_unit = -1
            max_sat_player = -1
            for u, bid_row in enumerate(self.bids):
                if GameData.units[u].owner == -1:
                    for p, bid in enumerate(bid_row):
                        sat = Pricing.comp_sat(bid_row, p)
                        if self.team_sizes[p] < self.max_team_size and max_sat < sat:
                            max_sat = sat
                            max_sat_unit = u
                            max_sat_player = p

            self.assign_unit(GameData.units[max_sat_unit], max_sat_player)
            print(f'{GameData.units[max_sat_unit].name:12s} to {max_sat_player} {self.players[max_sat_player]:12s}')

    # P x S
    def teams(self):
        teams = [[] for player in self.players]

        for unit in GameData.units:
            teams[unit.owner].append(unit)
        return teams

    def print_teams(self):
        print('\n---Teams---')
        for player in self.players:
            print(f'{player:12s}', end=' ')
        print()

        teams = self.teams()
        for i in range(self.max_team_size):
            for team in teams:
                print(f'{(team[i].name[:12]):12s}', end=' ')
            print()

        for price in self.handicaps():
            print(f'{price:5.2f}       ', end=' ')
        print()

    def print_teams_detailed(self):
        print('\n---Teams detailed---', end='')

        teams = self.teams()
        prices = self.handicaps()
        for team, player, price in zip(teams, self.players, prices):
            print(f'\n{player}')
            for member in team:
                print(f'{member.name:12s} | '
                      f'{GameData.promo_strings[member.promo_type]} | '
                      f'{GameData.chapters[member.join_chapter]:30s}')
            print(f'Handicap: {price:5.2f}')
        print()

    # How player i values player j's team. No adjustments
    def value_matrix(self):
        v_matrix = [([0] * len(self.players)) for player in self.players]

        for valuer_i, valuer_row in enumerate(v_matrix):
            for unit, bid_row in zip(GameData.units, self.bids):
                valuer_row[unit.owner] += bid_row[valuer_i]

        return v_matrix

    # Could avoid recalculating in some circumstances, but these are not common;
    # at minimum, when a unit is reassigned, need to check for synergy relationship with new teammates;
    # also when leaving a team; can't save from that unit's prior swap because other teammates may have changed.
    # Depends on synergy relationship graph density, but on tests with FE8:
    # 54993/55440 calls to synergy_matrix() required a recalculation, implying very few reassignments meet
    # the circumstances of having no former or current teammates as connected to any moving unit.
    # If no synergy relationships, much faster on FE6, but cannot conclude any speedup in general.
    # Could also only update rows/columns of affected players, but most time is all-player rotations
    def synergy_matrix(self):
        s_matrix = [([0] * len(self.players)) for player in self.players]

        teams = self.teams()
        for u_i in range(self.max_team_size):
            for u_j in range((u_i+1), self.max_team_size):
                for player_i, synergies in enumerate(self.synergies):
                    for player_j, team in enumerate(teams):
                        s_matrix[player_i][player_j] += synergies[team[u_i].ID][team[u_j].ID]

        return s_matrix

    def v_s_matrix(self):
        v_matrix = self.value_matrix()
        s_matrix = self.synergy_matrix()
        for v_row, s_row in zip(v_matrix, s_matrix):
            for i in range(len(v_row)):
                v_row[i] += s_row[i]
                v_row[i] = max(0, v_row[i])
        return v_matrix

    # Adjusted for synergy and redundancy
    def final_matrix(self):
        return Pricing.apply_redundancy(self.v_s_matrix(), self.bid_sums, self.opp_ratio)

    def handicaps(self):
        return Pricing.pareto_prices(self.final_matrix(), self.opp_ratio)

    # Sum of values adjusted for redundancy for each chapter.
    # Also adjusted for promo competition
    def value_matrix_by_chapter(self):
        v_matrix = [([0] * len(self.players)) for player in self.players]

        for unit_c in GameData.units_with_competitors:
            unit_c.set_current_competitors()

        for c, MC_row in enumerate(self.MC_matrix):
            for p_i in range(len(self.players)):
                team_values_this_chapter = [0] * len(self.players)

                for unit, uvpc in zip(GameData.units, self.unit_value_per_chapter):
                    if unit.join_chapter <= c:
                        if unit.current_competitors == 0:
                            team_values_this_chapter[unit.owner] += uvpc[p_i]
                        else:
                            team_values_this_chapter[unit.owner] += uvpc[p_i] * unit.get_late_promo_factor(c)
                    else:  # all subsequent units have not appeared yet
                        break

                for p_j in range(len(self.players)):
                    v_matrix[p_i][p_j] += Pricing.redundancy(
                        team_values_this_chapter[p_j], MC_row[p_i], self.opp_ratio)

        return v_matrix

    # Print matrix, comp_sat, handicaps, and matrix+sat after handicapping
    def print_value_matrices(self):
        def print_matrix(m, string):
            print()
            print(string)
            for p, row in enumerate(m):
                print(f'{self.players[p]:10s}', end=' ')
                for i, value in enumerate(row):
                    print(f' {value:6.2f}   ', end=' ')
                print(f' {Pricing.comp_sat(row, p):6.2f}')

        print()
        print('          ', end=' ')
        for player in self.players:
            print(f'{player:10s}', end=' ')
        print('Comparative satisfaction')

        print_matrix(self.value_matrix(), 'Unadjusted value matrix')

        print_matrix(self.synergy_matrix(), 'Synergy')

        print_matrix(self.v_s_matrix(), 'Synergy adjusted')

        final_matrix = self.final_matrix()
        print_matrix(final_matrix, 'Redundancy adjusted')

        robustness = 0
        for p, row in enumerate(final_matrix):
            for i, value in enumerate(row):
                if p == i:
                    robustness += value

        print()
        print(f'Average team robustness: {robustness/len(self.players):6.2f}')

        print('HANDICAPS:', end=' ')
        prices = self.handicaps()
        for price in prices:
            print(f' {price:6.2f}   ', end=' ')
        print()
        print()

        print('Handicap adjusted')
        for p, row in enumerate(final_matrix):
            print(f'{self.players[p]:10s}', end=' ')
            for value, price in zip(row, prices):
                print(f' {value - price:6.2f}   ', end=' ')
            print(f' {Pricing.comp_sat(row, p) - Pricing.comp_sat(prices, p):6.2f}')

    def get_score(self):
        return Pricing.allocation_score(self.final_matrix(), self.robust_factor)

    # try all swaps to improve score
    def improve_allocation_swaps(self):
        current_score = self.get_score()
        swapped = False

        for u_i, unit_i in enumerate(GameData.units):
            for unit_j in GameData.units[u_i+1:]:
                if unit_i.owner != unit_j.owner:
                    unit_i.set_owner(unit_j.owner)
                    unit_j.set_owner(unit_i.prior_owner)

                    if current_score < self.get_score():
                        current_score = self.get_score()
                        swapped = True
                        # Use name of owner before swap
                        print(f'Swapping {self.players[unit_j.owner]:12s} '
                              f'{(unit_i.name[:12]):12s} <-> {(unit_j.name[:12]):12s} '
                              f'{self.players[unit_i.owner]:12s}, '
                              f'new score {current_score:7.3f}')
                    else:  # return units to owners
                        unit_i.set_owner(unit_j.owner)
                        unit_j.set_owner(unit_i.prior_owner)
        return swapped

    # try all rotations (swaps of three or more) to improve score
    # iterate over rotations at the highest level,
    # skip branching tree if player at that level of recursion isn't trading
    # only full p rotations will cost much time

    # If this didn't rotate from rotations[test_until], only need to check until that point:
    # Complete one "lap" without any successful rotation, lap doesn't need to start at rotation[0]
    # Set last_rotation to index r whenever a rotation occurs to pass to next execution.
    def improve_allocation_rotate(self, test_until):
        start = time.time()

        current_score = self.get_score()
        last_rotation = -1

        indices = [0]*len(self.players)  # of units being traded from 0~teamsize-1, set during recursive_rotate branch
        teams = self.teams()

        def recursive_rotate(p_i):
            nonlocal teams
            nonlocal current_score
            nonlocal last_rotation

            if p_i >= len(self.players):  # base case
                for p in trading_players:
                    teams[p][indices[p]].set_owner(rotation[p])  # p's unit goes to rotation[p]

                if current_score < self.get_score():
                    current_score = self.get_score()

                    print('\nRotating:')
                    for p2 in trading_players:
                        print(f'{self.players[p2]:12s} -> '
                              f'{(teams[p2][indices[p2]].name[:12]):12s} -> '
                              f'{self.players[rotation[p2]]:12s}')
                    print(f'New score {current_score:7.3f}')
                    print()

                    while self.improve_allocation_swaps():
                        pass
                    current_score = self.get_score()

                    teams = self.teams()
                    last_rotation = r
                else:
                    for p in trading_players:
                        teams[p][indices[p]].set_owner(p)  # unrotate, if teams were updated rotates to new teams

            else:
                if p_i in trading_players:
                    for indices[p_i] in range(self.max_team_size):  # for each unit in the team
                        recursive_rotate(p_i + 1)
                else:  # don't branch, this player isn't trading in this rotation, go to next player
                    recursive_rotate(p_i + 1)

        for r, rotation in enumerate(self.rotations):
            if r > test_until and last_rotation < 0:
                print('Reached latest effected rotation of prior loop. Stopping rotation early.')
                return last_rotation

            trading_players = [p for p, r in enumerate(rotation) if p != r]
            print(f'{time.time() - start:7.2f} {r:3d}/{len(self.rotations):3d}  '
                  'Rotation ', rotation, '  Trading players ', trading_players)
            recursive_rotate(0)

        return last_rotation

    def run(self):
        self.print_bids()
        self.max_sat_assign()

        while self.improve_allocation_swaps():
            pass

        test_until = len(self.rotations)
        while test_until >= 0:
            test_until = self.improve_allocation_rotate(test_until)

        self.print_value_matrices()
        self.print_teams()

 

Pricing.py

Spoiler



# Comparative Satisfaction.
# In a zero-sum game, subtract average opponent's perceived value from own.
def comp_sat(values, my_i):
    avg_opp_value = sum(values)
    avg_opp_value -= values[my_i]
    avg_opp_value /= (len(values) - 1)
    return values[my_i] - avg_opp_value


# Compensate for unit redundancies.
# If the worst team can complete a chapter in 3 turns,
# then no team can save more than 2 turns. Nevertheless,
# there may be three or more units that each individually
# save one turn, yet all together cannot save 3 turns.
# Together, they save less than the sum of their parts.
# the following function R satisfies:
# R(0) = 0
# R(inf) = max_v
# R(max_v/#players) = max_v/#players; no adjustment for average value
def redundancy(value, max_v, opp_ratio):
    try:
        return (value * max_v) / (value + max_v * opp_ratio)
    except ZeroDivisionError:
        return 0


def apply_redundancy(value_matrix, max_values, opp_ratio):
    for i in range(len(value_matrix)):
        for j in range(len(value_matrix)):
            value_matrix[i][j] = redundancy(value_matrix[i][j], max_values[i], opp_ratio)
    return value_matrix


# Finds prices that produce equalized satisfaction.
# A's satisfaction equals Handicapped Team Value - average opponent's HTV
# (from A's subjective perspective)
def pareto_prices(value_matrix, opp_ratio):
    sat_values = [comp_sat(row, i) for i, row in enumerate(value_matrix)]
    return [(value - min(sat_values))*opp_ratio for value in sat_values]


# Try to maximize net satisfaction.
# However, for sufficiently similar bids,
# degenerate results may optimize naive net satisfaction.
# Add slight preference for each player considering their own team good:
# If a change would cause each player to think they would finish 8 turns sooner,
# but 1 turn later relative to average opponent, that change is neutral.
# Testing with different values shows that 1/8 has very little effect,
# values around 1 have large effect and produce close to equal team self assessment
# Seems to have low cost to satisfaction?
def allocation_score(value_matrix, robust_factor):
    score = 0
    for i, row in enumerate(value_matrix):
        score += comp_sat(row, i) + row[i]*robust_factor
    return score


# If a permutation has one loop longer than two
# (because swaps are already covered) then we
# want to test it. If there is more than one loop,
# we don't need to test it because we already tested
# the loops individually
def just_one_loop(permutation):
    players_trading = 0
    highest_trading_player = 0
    for index, item in enumerate(permutation):
        if index != item:
            players_trading += 1
            highest_trading_player = index

    if players_trading < 3:
        return False

    def highest_loop_member(x):
        record = x

        def recursive(y):
            nonlocal record
            if record < y:
                record = y
            if permutation[y] == x:
                return record
            return recursive(permutation[y])

        return recursive(x)

    for index, item in enumerate(permutation):
        if highest_loop_member(index) < highest_trading_player and index != item:
            return False

    return True

 

GameData.py

Spoiler

promo_KC = 0  # knight crest
promo_HC = 1  # hero crest
promo_OB = 2  # orion's bolt
promo_EW = 3  # elysian whip
promo_GR = 4  # guiding ring
promo_O8 = 5  # ocean seal in FE8
promo_ES = 6  # earth seal, item only, leave room to insert ocean seal in FE8
promo_OS = 7  # ocean seal
promo_FC = 8  # fell contract
promo_HS = 9  # heaven seal
promo_NO = 10  # can't promote

promo_strings = [
    'Nite ',
    'Hero ',
    'Bolt ',
    'Whip ',
    'Ring ',
    'Ocean',
    'Earth',
    'Ocean',
    'Fell ',
    'Heven',
    '     '
]

chapters_FE6 = [
    ' 1  Dawn of Destiny',
    ' 2  Princess of Bern',
    ' 3  Late Arrival',
    ' 4  Collapse of the Alliance',
    ' 5  Fire Emblem',
    ' 6  Trap'
    ' 7  Rebellion of Ostia',
    ' 8  Reunion',
    ' 8x Blazing Sword',
    ' 9  Misty Isles',
    '10  Resistance Forces/Caught in the Middle',
    '11  Hero of the Western Isles/Escape to Freedom',
    '12  True Enemy',
    '12x Axe of Thunder',
    '13  Rescue Plan',
    '14  Arcadia',
    '14x Infernal Element',
    '15  Dragon Girl',
    '16  Retaking the Capital',
    '16x Pinnacle of Light',
    '17  Bishop\'s Teachings/Path Through the Ocean',
    '18  Law of Sacae/Frozen River',
    '19  Battle in Bulgar/Bitter Cold',
    '20  Silver Wolf/Liberation of Ilia',
    '20x Bow of the Winds/Spear of Ice',
    '21  Sword of Seals',
    '21x Silencing Darkness',
    '22  Neverending Dream',
    '23  Ghost of Bern',
    '24  Truth of the Legend',
    '25  Beyond the Darkness'
]

chapters_FE7 = [
    '11  Another Journey',
    '12  Birds of a Feather',
    '13  In Search of Truth',
    '13x The Peddler Merlinus',
    '14  False Friends',
    '15  Talons Alight',
    '16  Noble Lady of Caelin',
    '17  Whereabouts Unknown',
    '17x The Port of Badon',
    '18  Pirate Ship',
    '19  The Dread Isle',
    '19x Imprisoner of Magic',
    "20  Dragon's Gate",
    '21  New Resolve',
    "22  Kinship's Bond",
    '23  Living Legend',
    '23x Genesis',
    '24  Four-Fanged Offense',
    '25  Crazed Beast',
    '26  Unfulfilled Heart',
    '27  Pale Flower of Darkness',
    '28  Battle Before Dawn',
    '28x Night of Farewells',
    '29  Cog of Destiny',
    '30  The Berserker',
    '31  Sands of Time',
    '31x Battle Preparations',
    '32  Victory or Death',
    '32x The Value of Life',
    '33  Light'
]

chapters_FE8_Eirika = [
    'Prologue: The Fall of Renais',
    ' 1  Escape!',
    ' 2  The Protected',
    ' 3  The Bandits of Borgo',
    ' 4  Ancient Horrors',
    " 5  The Empire's Reach",
    ' 5x Unbroken Heart',
    ' 6  Victims of War',
    ' 7  Waterside Renvall',
    " 8  It's a Trap!",
    ' 9A Distant Blade',
    '10A Revolt at Carcino',
    '11A Creeping Darkness',
    '12A Village of Silence',
    "13A Hamill Canyon",
    '14A Queen of White Dunes',
    '15  Scorched Sand',
    '16  Ruled by Madness',
    '17  River of Regrets',
    '18  Two Faces of Evil',
    '19  Last Hope',
    '20  Darkling Woods',
    '21  Sacred Stone'
]

chapters_FE8_Ephraim = [
    'Prologue: The Fall of Renais',
    ' 1  Escape!',
    ' 2  The Protected',
    ' 3  The Bandits of Borgo',
    ' 4  Ancient Horrors',
    " 5  The Empire's Reach",
    ' 5x Unbroken Heart',
    ' 6  Victims of War',
    ' 7  Waterside Renvall',
    " 8  It's a Trap!",
    ' 9B Fort Rigwald',
    '10B Turning Traitor',
    '11B Phantom Ship',
    '12B Landing at Taizel',
    "13B Fluorspar's Oath",
    '14B Father and Son',
    '15  Scorched Sand',
    '16  Ruled by Madness',
    '17  River of Regrets',
    '18  Two Faces of Evil',
    '19  Last Hope',
    '20  Darkling Woods',
    '21  Sacred Stone'
]

chapters = chapters_FE8_Eirika

promo_item_acquire_times_FE7_HNM = [
    # Entries that appear with a line break between
    # their nominal acquire chapter indicate that they
    # are more likely to not be helpful in that chapter,
    # due to location or time they appear.

    # chapter
    # 11 / 0
    # 12 / 1
    # 13 / 2
    # 13x / 3
    # 14 / 4
    # 15 / 5
    # 16 / 6
    # 17 / 7

    {'chapter': 8, 'type': promo_KC, 'number': 1},  # Whereabouts Unknown, chest
    {'chapter': 8, 'type': promo_HC, 'number': 1},  # Whereabouts Unknown, chest
    # 17x / 8

    # 18 / 9

    {'chapter': 10, 'type': promo_GR, 'number': 1},  # Pirate Ship, shaman
    # 19 / 10

    {'chapter': 11, 'type': promo_OB, 'number': 1},  # The Dread Isle, Uhai
    # 19x / 11
    # 20 / 12

    {'chapter': 13, 'type': promo_HC, 'number': 1},  # New Resolve, chest
    # 21 / 13

    {'chapter': 14, 'type': promo_EW, 'number': 1},  # New Resolve, village
    {'chapter': 14, 'type': promo_HC, 'number': 1},  # New Resolve, Oleg (steal)
    # 22 / 14
    {'chapter': 14, 'type': promo_KC, 'number': 1},  # Kinship's Bond, cavalier

    # 23 / 15
    {'chapter': 15, 'type': promo_OS, 'number': 1},  # Living Legend, close sand
    # (can get from shops later but never need more than 1)

    {'chapter': 16, 'type': promo_HC, 'number': 1},  # Living Legend, far sand
    {'chapter': 16, 'type': promo_GR, 'number': 1},  # Living Legend, Jasmine (steal)
    # 23x / 16
    # 24 / 17

    {'chapter': 18, 'type': promo_ES, 'number': 1},  # Four-Fanged Offense, village
    {'chapter': 18, 'type': promo_OB, 'number': 1},  # Four-Fanged Offense, village A, Sniper B
    # {'chapter: 18, 'type': promo_OS, 'number': 9},  # Four-Fanged Offense A ONLY, secret shop
    # 25 / 18

    {'chapter': 19, 'type': promo_EW, 'number': 1},  # Crazed Beast, village
    # 26 / 19
    {'chapter': 19, 'type': promo_HS, 'number': 1},  # Unfulfilled Heart, auto at start

    # 27 / 20

    {'chapter': 21, 'type': promo_GR, 'number': 1},  # Pale Flower of Darkness A ONLY, chest
    {'chapter': 21, 'type': promo_HC, 'number': 1},  # Pale Flower of Darkness B ONLY, chest
    # 28 / 21

    {'chapter': 22, 'type': promo_EW, 'number': 1},  # Battle Before Dawn, bishop
    {'chapter': 22, 'type': promo_HS, 'number': 1},  # Battle Before Dawn, auto at chapter end
    # 28x / 22

    {'chapter': 23, 'type': promo_FC, 'number': 1},  # Night of Farewells, Sonia, free chapter
    # 29 / 23

    {'chapter': 24, 'type': promo_GR, 'number': 1},  # Cog of Destiny, sniper (steal)
    # 30 / 24
    # 31 / 25

    {'chapter': 26, 'type': promo_KC, 'number': 9},  # Sands of Time, secret shop, survive chapter
    {'chapter': 26, 'type': promo_HC, 'number': 9},
    {'chapter': 26, 'type': promo_OB, 'number': 9},
    {'chapter': 26, 'type': promo_EW, 'number': 9},
    {'chapter': 26, 'type': promo_GR, 'number': 9},
    # 31x / 26
    # 32 / 27
    {'chapter': 27, 'type': promo_ES, 'number': 1},  # Victory or Death, nils at start

    {'chapter': 28, 'type': promo_OS, 'number': 9},  # Victory or Death, secret shop at end
    {'chapter': 28, 'type': promo_FC, 'number': 9},
    {'chapter': 28, 'type': promo_ES, 'number': 9}
]

promo_item_acquire_times_FE8 = [
    # P/ 1
    # 1/ 2
    # 2/ 3
    # 3/ 4
    # 4/ 5
    # 5/ 6

    { 8, promo_GR, 1},  # if all villages visited, only usable on ch6
    #5x/ 7
    # 6/ 8

    { 9, promo_OB, 1},  # if civs survive
    # 7/ 9

    {10, promo_KC, 1},  # Murray
    # 8/10

    {11, promo_EW, 1},  # chest
    # 9/11
    {11, promo_O8, 1},  # a pirate, b chest

    #10/12
    #{12, promo_HC, 1}, # b ONLY, village
    {12, promo_HC, 1},  # 10a or 13b, Gerik

    {13, promo_GR, 1},  # a ONLY, Pablo
    #{13, promo_KC, 1}, # b ONLY, if all npc cavs survive
    #11/13
    #12/14
    #{14, promo_GR, 1}, # b ONLY, shaman

    #13/15
    {15, promo_EW, 1},  # 13a or 10b, Cormag

    {16, promo_KC, 1},  # a ONLY, Aias
    #14/16
    {16, promo_GR, 1},  # chest

    {16, promo_HC, 1},  # a ONLY, myrmidon

    #{17, promo_KC, 1}, # b ONLY, Vigarde
    #15/17
    {17, promo_ES, 1},  # village
    {17, promo_GR, 1},  # steal shaman

    #16/18
    {18, promo_KC, 1},  # chest
    {18, promo_HC, 1},  # enemy

    {19, promo_HS, 2},
    #17/19
    {19, promo_GR, 1}  # mage

    #18/20
    #19/21
    #20/22
    #21/23
]

# chapter x item_types running total of available items
promo_item_count = []
for c in range(len(chapters)):
    promo_item_count.append([0] * len(promo_strings))

# assume that no more than 1 earth seal will be used,
# add to count for all items it can substitute for
for entry in promo_item_acquire_times_FE7_HNM:
    for row in promo_item_count[entry['chapter']:]:
        row[entry['type']] += entry['number']

        if entry['type'] == promo_ES:
            for t in range(promo_ES):
                row[t] += entry['number']

unit_data_FE6 = [
    ['Lance', 1, promo_KC],
    ['Alan', 1, promo_KC],
    ['Wolt', 1, promo_OB],
    ['Bors', 1, promo_KC],
    ['Shanna', 2, promo_EW],
    ['Dieck', 2, promo_HC],
    ['Lott', 2, promo_HC],
    ['Wade', 2, promo_HC],
    ['Ellen', 2, promo_GR],
    ['Lugh', 3, promo_GR],
    ['Chad', 3, promo_NO],
    ['Rutger', 4, promo_HC],
    ['Clarine', 4, promo_GR],
    ['Marcus>=Ch6', 6, promo_GR],
    ['Saul', 6, promo_GR],
    ['Sue', 6, promo_OB],
    ['Dorothy', 6, promo_OB],
    ['Zealot', 7, promo_NO],
    ['Treck', 7, promo_KC],
    ['Noah', 7, promo_KC],
    ['Astohl', 8, promo_NO],
    ['Oujay', 8, promo_HC],
    ['Barth', 8, promo_KC],
    ['Wendy', 8, promo_KC],
    ['Lilina', 8, promo_GR],
    ['Shin', 10, promo_OB],
    ['Fir', 10, promo_HC],
    ['Gonzales', 11, promo_HC],
    ['Geese', 11, promo_HC],
    ['Klein', 12, promo_NO],
    ['Tate', 12, promo_EW],
    ['Echidna', 12, promo_NO],
    ['Bartre', 12, promo_NO],
    ['Ray', 13, promo_GR],
    ['Cath', 13, promo_NO],
    ['Miledy', 15, promo_EW],
    ['Perceval', 15, promo_NO],
    ['Cecelia', 16, promo_NO],
    ['Sophia', 16, promo_GR],
    ['Igrene', 18, promo_NO],
    ['Garret', 18, promo_NO],
    ['Fa', 19, promo_NO],
    ['Hugh', 19, promo_GR],
    ['Ziess', 19, promo_EW],
    ['Douglas', 20, promo_NO],
    ['Niime', 23, promo_NO],
    ['Juno', 24, promo_NO],
    ['Dayan', 24, promo_NO],
    ['Yodel', 26, promo_NO],
    ['Karel', 29, promo_NO]
]

unit_data_FE7_HNM_split_Marcus = [
    # chapter
    # 11 / 0
    # 12 / 1
    ['Matthew', 1, promo_FC],  # free for 11 / 1
    ['Serra', 1, promo_GR],
    ['Oswin', 1, promo_KC], # Will Oswin or Lowen be promoted first?
    ['Eliwood', 1, promo_HS],
    ['Lowen', 1, promo_KC],
    ['Rebecca', 1, promo_OB],
    ['Dorcas', 1, promo_HC],
    ['Bartre&Karla', 1, promo_HC],
    ['Marcus<=19x', 1, promo_NO],
    # 13 / 2
    ['Guy', 2, promo_HC],
    # 13x / 3
    # 14 / 4
    ['Erk', 4, promo_GR],
    ['Priscilla', 5, promo_GR],  # unlikely to contribute in join chapter
    # 15 / 5
    # 16 / 6
    ['Florina', 6, promo_EW],
    ['Lyn', 7, promo_HS],  # free during join chapter
    ['Sain', 7, promo_KC],
    ['Kent', 7, promo_KC],
    ['Wil', 7, promo_OB],
    # 17 / 7
    ['Raven', 7, promo_HC],
    ['Lucius', 8, promo_GR],  # unlikely to contribute in join chapter
    # 17x / 8
    ['Canas', 8, promo_GR],
    # 18 / 9
    # 19 / 10
    ['Dart', 10, promo_OS],
    ['Fiora', 10, promo_EW],
    # 19x / 11
    # 20 / 12
    ['Marcus>=20', 12, promo_NO],
    ['Legault', 12, promo_FC],  # unlikely to contribute in join chapter
    # 21 / 13
    # 22 / 14
    ['Isadora', 14, promo_NO],
    ['Heath', 15, promo_EW],
    ['Rath', 15, promo_OB],
    # 23 / 15
    ['Hawkeye', 15, promo_NO],
    # 23x / 16
    # 24 / 17
    # ['Wallace/Geitz', 17, promo_NO],
    # 25 / 18
    ['Farina', 18, promo_EW],
    # 26 / 19
    ['Pent', 19, promo_NO],
    ['Louise', 19, promo_NO],
    # 27 / 20
    # ['Harken', 20, promo_NO],
    # ['Karel', 20, promo_NO],
    # 28 / 21
    ['Nino', 21, promo_GR],
    # 28x / 22
    ['Jaffar', 22, promo_NO],
    # 29 / 23
    ['Vaida', 23, promo_NO],
    # 30 / 24
    # 31 / 25
    # 31x / 26
    # 32 / 27
    ['Renault', 27, promo_NO]
    # 32x / 28
    # 33 / 29
    # ['Athos', 29, promo_NO]
]

unit_data_FE7_HNM = [
    # chapter
    # 11 / 0
    # 12 / 1
    ['Matthew', 1, promo_FC],  # free for 11 / 1
    ['Serra', 1, promo_GR],
    ['Oswin', 1, promo_KC], # Will Oswin or Lowen be promoted first?
    ['Eliwood', 1, promo_HS],
    ['Lowen', 1, promo_KC],
    ['Rebecca', 1, promo_OB],
    ['Dorcas', 1, promo_HC],
    ['Bartre&Karla', 1, promo_HC],
    # 13 / 2
    ['Guy', 2, promo_HC],
    # 13x / 3
    # 14 / 4
    ['Erk', 4, promo_GR],
    ['Priscilla', 5, promo_GR],  # unlikely to contribute in join chapter
    # 15 / 5
    # 16 / 6
    ['Florina', 6, promo_EW],
    ['Lyn', 6, promo_HS],  # no longer free, restricted to box
    ['Sain', 6, promo_KC],
    ['Kent', 6, promo_KC],
    ['Wil', 6, promo_OB],
    # 17 / 7
    ['Raven', 7, promo_HC],
    ['Lucius', 8, promo_GR],  # unlikely to contribute in join chapter
    # 17x / 8
    ['Marcus>=17x', 8, promo_NO],
    ['Canas', 8, promo_GR],
    # 18 / 9
    # 19 / 10
    ['Dart', 10, promo_OS],
    ['Fiora', 10, promo_EW],
    # 19x / 11
    # 20 / 12
    ['Legault', 12, promo_FC],  # unlikely to contribute in join chapter
    # 21 / 13
    # 22 / 14
    ['Isadora', 14, promo_NO],
    ['Heath', 15, promo_EW],
    ['Rath', 15, promo_OB],
    # 23 / 15
    ['Hawkeye', 15, promo_NO],
    # 23x / 16
    # 24 / 17
    # ['Wallace/Geitz', 17, promo_NO],
    # 25 / 18
    ['Farina', 18, promo_EW],
    # 26 / 19
    ['Pent', 19, promo_NO],
    ['Louise', 19, promo_NO],
    # 27 / 20
    # ['Harken', 20, promo_NO],
    # ['Karel', 20, promo_NO],
    # 28 / 21
    ['Nino', 21, promo_GR],
    # 28x / 22
    ['Jaffar', 22, promo_NO],
    # 29 / 23
    ['Vaida', 23, promo_NO],
    # 30 / 24
    # 31 / 25
    # 31x / 26
    # 32 / 27
    ['Renault', 27, promo_NO],
    # 32x / 28
    # 33 / 29
    # ['Athos', 29, promo_NO]
    ['--none--', 29, promo_NO]
]

unit_data_FE8 = [
    # P/ 0
    # ['Eirika',	0, promo_HS],
    # 1
    ['Franz', 1, promo_KC],
    ['Gilliam', 1, promo_KC],
    # 2
    ['Vanessa', 2, promo_EW],
    ['Moulder', 2, promo_GR],
    ['Ross', 2, promo_O8],  # can promo_HC
    ['Garcia', 3, promo_HC],
    # 3
    ['Neimi', 3, promo_OB],
    ['Colm', 3, promo_O8],
    # 4
    ['Artur', 4, promo_GR],
    ['Lute', 4, promo_GR],
    # 5
    ['Seth>=5', 5, promo_NO],
    ['Natasha', 5, promo_GR],
    ['Joshua', 5, promo_HC],
    # 5x/ 6
    # ['Orson',		6, promo_NO],
    # 6/ 7
    # 7/ 8
    # 8/ 9
    ['Forde', 9, promo_KC],
    ['Kyle', 9, promo_KC],
    # 9/10
    ['Tana', 10, promo_EW],  # same in both routes, mostly unuseable in eph9
    ['Amelia', 10, promo_KC],  # returns in eir 13
    # 10/11
    ['Gerik', 11, promo_HC],  # 13/15 +3 eph
    # ['Tethys',	11, promo_NO], # 13/15 +3 eph
    ['Innes', 11, promo_NO],  # 15/17 +5 eph
    ['Marisa', 11, promo_HC],  # 12/14 +2 eph
    # 11/12
    ['Dozla', 12, promo_NO],
    ["L'Arachel",	12, promo_GR],
    # 12/13
    ['Saleh', 13, promo_NO],  # 15/17 +3 eph
    ['Ewan', 13, promo_GR],
    # 13/14
    ['Cormag', 14, promo_EW],  # 10/12 -3 eph
    # 14/15
    ['Rennac', 15, promo_NO],
    # 15/16
    ['Duessel', 16, promo_NO],  # 10/12 -5 eph
    ['Knoll', 16, promo_GR],
    # 16/17
    ['2nd Lord', 17, promo_HS],
    ['Myrrh', 17, promo_NO],
    # 17/18
    ['Syrene', 18, promo_NO],
    # 18/19
    # 19/20
    # 20/21
    # 21/22
    ['Bonus Units', 22, promo_NO]
]

unit_data = unit_data_FE8


class Unit:
    def __init__(self, ID, data_i):
        self.ID = ID
        self.name = data_i[0]
        self.join_chapter = data_i[1]
        self.promo_type = data_i[2]
        self.owner = -1
        self.prior_owner = -1
        self.late_promo_factors = []  # same_promo_priors x chapters
        self.earliest_promo = -1
        self.competitors = set()
        self.current_competitors = 0

    def set_owner(self, new_owner):
        self.prior_owner = self.owner
        self.owner = new_owner

    def set_current_competitors(self):
        self.current_competitors = 0
        for comp in self.competitors:
            if self.owner == comp.owner:
                self.current_competitors += 1

    def get_late_promo_factor(self, chapter):
        return self.late_promo_factors[self.current_competitors-1][chapter]


units_with_competitors = set()

units = [Unit(ID, data) for ID, data in enumerate(unit_data)]
for u, unit_prior in enumerate(units):
    for c, row in enumerate(promo_item_count):
        if row[unit_prior.promo_type] > 0:
            unit_prior.earliest_promo = max(unit_prior.join_chapter, c)
            break

    # add an entry for each prior unit in same promotion class
    for unit_post in units[u + 1:]:
        if unit_prior.promo_type == unit_post.promo_type and unit_prior.promo_type != promo_NO:
            units_with_competitors.add(unit_post)
            unit_post.competitors.add(unit_prior)
            unit_post.late_promo_factors.append([1] * len(chapters))

# reduce late_promo_factor for competition
for unit in units:
    for prior_competitors in range(1, len(unit.late_promo_factors) + 1):
        factor = 1

        # start from first promotable chapter
        for c in range(unit.earliest_promo, len(chapters)):
            if promo_item_count[c][unit.promo_type] <= prior_competitors:
                factor = max(factor - 0.125, 0)
                unit.late_promo_factors[prior_competitors - 1][c] = factor
            else:
                break  # gained item or already had it, other factors in row remain 1

 

 

Edited by JudgeWargrave
Pasting in Python script
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...