JudgeWargrave Posted October 19, 2019 Share Posted October 19, 2019 (edited) 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 October 19, 2019 by JudgeWargrave Pasting in Python script Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.