def build_safety_game(g, j, lam): """ Build the game on which we will solve a safety objective. :param g: the game to solve. :param j: the player for which we want to create the new game. :param lam: the maximum size of a window. :return: g' the new game created. Note that we don't return beta like we do in the theory because we define a unique id for beta that is -1. """ g_new = Graph() for s in g.get_nodes(): # O(n) * O(d) * O(lambda) for c in range(0, ops.max_priority(g) + 1): #O(d) * O(lambda) for l in range(0, lam): #O(lambda) s_player = g.get_node_player(s) #node id is the concatenation of s, c and l plus letters to make the separation. # Ex : if we have a node s = 5, c = 10, l = 2 we would have the node id = s5c10l2. #this way we have unique id and we can easily find a specific node if we want to node_id = compute_id(s, c, l) #node informations are in the following format : (player, base node id, maximum color on the current window, step on current window) aka (s_player, s, c, l) g_new.add_node(node_id, (s_player, s, c, l)) #add the state beta that detects the lambda-bad windows. We use a special identification and descriptor for this state g_new.add_node(-1, (-1, -1, -1)) g_new.add_successor(-1, -1) g_new.add_predecessor(-1, -1) #iterate on all the node of g_new for node_id in g_new.get_nodes( ): # (O(m) * O(d) * O(lambda)) (the two loops make a complexity O(m)) if node_id != -1: (s, c, l) = g_new.nodes[node_id][1:4] trans_list = g.get_successors(s) #iterate on all successors (in g) of the node s for s2 in trans_list: #when we encounter a c of parity j we know we can reset the window and go to the next state. So we add the corresponding transition in the new game if ((c % 2) == j): #we want to add a transition from (s, c, l) towards (s2, c2, l2) #compute c2 to be the color of s2 c2 = g.get_node_priority(s2) #compute the id of the node we want to add a transition toward node_id_trans = compute_id(s2, c2, 0) #when we are just continuing on the current window elif ((c % 2) != j and l < (lam - 1)): #we want to add a transition from (s, c, l) towards (s2, c2, l2) #compute c2 to be the maximum between c and the color of s2 c2 = max(c, g.get_node_priority(s2)) #compute the id of the node we want to add a transition toward node_id_trans = compute_id(s2, c2, l + 1) #when we just detected a lambda-bad window else: #we want to add a transition from (s, c, l) towards beta node_id_trans = -1 #we add the transition we just compute to g_new g_new.add_successor(node_id, node_id_trans) g_new.add_predecessor(node_id_trans, node_id) return (g_new)
def get_j_colors(g, j): """ Give all the colors of parity j on the game g :param g: the game to solve. :param j: the player for who we want to get the colors. :return: all the colors of parity j on the game g. """ d = ops.max_priority(g) j_colors = [] for i in range(1, d + 1): if i % 2 == j: j_colors.append(i) return j_colors
def compute_gdagger(g, j): """ Compute and return the product game g_dagger from g for the player j. Total complexity : O(n) + O(n*d) + O(n*d) * O(n) = O(n^2*d) :param g: the game on which g_dagger is based. :param j: the player for who g_dagger is computed. :return: the product game g_dagger from g for the player j. """ #creating a list containing all colors in the game d = ops.max_priority(g) #O(n) colors = range(0, d + 1) g_dagger = Graph() #adding all states in g_dagger. The stated are created doing a cartesian product between g states and colors for s in g.get_nodes(): #O(n*d) for v in colors: #O(d) #node_id is a concatenation of the node id in g and the color v that will be added in the node_info with letter to identify each part. #Ex: s = 10, v = 8 => node_id = s10v8 node_id = compute_id(s, v) #computing the color of the state s_color = g.get_node_priority(s) node_color = -1 if v % 2 == j: node_color = s_color else: node_color = v #player of s is the same as the player of the node we are creating s_player = g.get_node_player(s) #node info take this form : (player, base node id, color computed, v : supposed to be the maximal color encoutered on the path) node_info = (s_player, s, node_color, v) #add the node in g_dagger g_dagger.add_node(node_id, node_info) #now adding the trasitions between the states. #we add a trasition ((s1, v1), (s2, v2)) iff (s1, s2) is a transition in g and if v2 = max(v1, c(s2)) for s1 in g_dagger.get_nodes(): #O(n*d) * O(n) = O(n^2 * d) #getting the id of the corresponding node in g s1_id_g = g_dagger.nodes[s1][1] for s2 in g.get_successors(s1_id_g): #O(n) #getting the color of s2 in g s2_color = g.get_node_priority(s2) #getting v1 from the state s1 v1 = g_dagger.nodes[s1][3] v2 = max(v1, s2_color) #with that we've got (s2, v2) towards which we want to add a transition from s1 (s1 is already a state of g_dagger we can considerate it as (s1_id_g, v1)) #getting the node id of (s2, v2) in g_dagger node_id_trans = compute_id(s2, v2) g_dagger.add_successor(s1, node_id_trans) #amortized O(1) g_dagger.add_predecessor(node_id_trans, s1) #amortized O(1) return g_dagger
def weak_parity_solver(g): """ Weak parity games solver. This is an implementation of the algorithm presented in chapter 4 of the report. :param g: the game to solve. :return: the solution in the following format : (W_0, sigma_0), (W_1, sigma_1). """ h = g # the game we work on i = ops.max_priority(h) # Maximum priority occurring in g W0 = [] # winning region for player 0 W1 = [] # winning region for player 1 sigma0 = defaultdict(lambda: -1) # winning strategy for player 0 sigma1 = defaultdict(lambda: -1) # winning strategy for player 1 # considering priorities from i to 0 in decreasing order for k in range(i, -1, -1): current_player = k % 2 # get current player # calling the reachability solver on the game h with target set "nodes of priority k" and for the current player (Ak, eta), (Bk, nu) = rs.reachability_solver(h, ops.i_priority_node(h, k), current_player) # depending on the current player, we add the nodes of Ak in a winning region and update strategies if current_player == 0: W0.extend(Ak) sigma0.update(eta) sigma1.update(nu) else: W1.extend(Ak) sigma1.update(eta) sigma0.update(nu) h = h.subgame( Bk) # updates the current game (only keeping nodes in Bk) return (W0, sigma0), (W1, sigma1)
def disj_parity_win2(g, maxValues, k, u): """ Recursive solver for generalized parity games. Uses the algorithm presented in http://www2.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-144.html This is used for testing purposes. :param g: the game to solve :param maxValues: the maximum value for each priority function :param k: the number of priority functions :param u: integer for testing purposes :return: W1, W2 the winning regions in the game for player 1 and player 2 (for the base game) """ # Base case : all maxValues are 1 or the game is empty if all(value == 1 for value in maxValues) or len(g.nodes) == 0: #print(str(u*2*" ")+"it-"+str(u)+" return on base case") return g.get_nodes(), [] for i in range(k): max = ops.max_priority(g) if max % 2 == 0: even = max attMaxOdd, compl_attMaxOdd = [], [] #print(str(u*2*" ")+"it-"+str(u)+" maxOdd-"+str(maxValues[i])+" attMaxOdd-"+str(attMaxOdd)+" "+"complAttMaxOdd-"+str(compl_attMaxOdd)) G1 = g attMaxEven, compl_attMaxEven = reachability.attractor( G1, ops.i_priority_node_function_j(G1, max, i + 1), 1) #print(str(u*2*" ")+"it-"+str(u)+" maxEven-"+str(maxValues[i]-1)+" attMaxEven-"+str(attMaxEven)+" "+"complAttMaxEven-"+str(compl_attMaxEven)) H1 = G1.subgame(compl_attMaxEven) j = 0 #print(str(u*2*" ")+"it-"+str(u)+" G\n"+str(G1)) #print(str(u*2*" ")+"it-"+str(u)+" H\n"+str(H1)) else: even = max - 1 attMaxOdd, compl_attMaxOdd = reachability.attractor( g, ops.i_priority_node_function_j(g, max, i + 1), 0) # print(str(u*2*" ")+"it-"+str(u)+" maxOdd-"+str(maxValues[i])+" attMaxOdd-"+str(attMaxOdd)+" "+"complAttMaxOdd-"+str(compl_attMaxOdd)) G1 = g.subgame(compl_attMaxOdd) attMaxEven, compl_attMaxEven = reachability.attractor( G1, ops.i_priority_node_function_j(G1, max - 1, i + 1), 1) # print(str(u*2*" ")+"it-"+str(u)+" maxEven-"+str(maxValues[i]-1)+" attMaxEven-"+str(attMaxEven)+" "+"complAttMaxEven-"+str(compl_attMaxEven)) H1 = G1.subgame(compl_attMaxEven) j = 0 # print(str(u*2*" ")+"it-"+str(u)+" G\n"+str(G1)) # print(str(u*2*" ")+"it-"+str(u)+" H\n"+str(H1)) while True: j += 1 copy_maxValues = copy.copy(maxValues) copy_maxValues[i] -= even - 1 W1, W2 = disj_parity_win2(H1, copy_maxValues, k, u + 1) #print(str(u * 2 * " ") + "it-" + str(u)+"-"+str(j) + " W1-" + str(W1) + " W2-" + str(W2)) #print("W1 "+str(W1)) #print("W2 "+str(W2)) #print("game " + str(g) + "att " + str(attMaxOdd) + "compl " + str(compl_attMaxOdd)) #print("stop "+str(set(W2))+" "+str(set(H1.get_nodes()))+" val "+ str(set(W2) == set(H1.get_nodes()))) #break # cette cond etait en dessous de lautre et lautre prennait precedence quand on avait les 2 #print(len(G1.nodes)) if len(G1.nodes) == 0: #print("G empty") break if set(W2) == set(H1.get_nodes()): #print("hello") B, compl_B = reachability.attractor(g, G1.get_nodes(), 1) W1, W2 = disj_parity_win2(g.subgame(compl_B), maxValues, k, u + 1) #print("re "+str(B)+" "+str(W1)+" "+str(W2)) B.extend(W2) return W1, B #break T, compl_T = reachability.attractor(G1, W1, 0) G1 = G1.subgame(compl_T) E, compl_E = reachability.attractor( G1, ops.i_priority_node_function_j(g, even, i + 1), 0) H1 = G1.subgame(compl_E) #break #break return g.get_nodes(), []
def strong_parity_solver_no_strategies(g): """ Strong parity games solver. This is an implementation of the recursive algorithm used to solve parity games. This implementation does not compute the winning strategies (for comparison purpose with other algorithms which don't) :param g: the game to solve. :return: the solution in the following format : (W_0, sigma_0), (W_1, sigma_1). """ W1 = [] # Winning region of player 0 W2 = [] # Winning region of player 1 # if the game is empty, return the empty regions if len(g.nodes) == 0: return W1, W2 else: i = ops.max_priority(g) # get max priority occurring in g # determining which player we are considering, if i is even : player 0 and else player 1 if i % 2 == 0: j = 0 else: j = 1 opponent = ops.opponent(j) # getting the opponent of the player U = ops.i_priority_node(g, i) # target set for the attractor : nodes of priority i # getting the attractor A and discarding the region for the opponent A, discard1 = reachability.attractor(g,U,j) # The subgame G\A is composed of the nodes not in the attractor, thus the nodes of the opposite player's region G_A = g.subgame(discard1) # Recursively solving the subgame G\A, solution comes as (W_0, W_1) sol_player1, sol_player2 = strong_parity_solver_no_strategies(G_A) # depending on which player we are considering, assign regions to the proper variables # W'_j is noted W_j, sigma'_j is noted sig_j; the same aplies for jbar if j == 0: W_j = sol_player1 W_jbar = sol_player2 else: W_j = sol_player2 W_jbar = sol_player1 # if W'_jbar is empty we update the regions depending on the current player # the region for the whole game for one of the players is empty if not W_jbar: if j == 0: W1.extend(A) W1.extend(W_j) else: W2.extend(A) W2.extend(W_j) else: # compute attractor B B, discard1 = reachability.attractor(g, W_jbar, opponent) # The subgame G\B is composed of the nodes not in the attractor, so of the opposite player's winning region G_B = g.subgame(discard1) # recursively solve subgame G\B, solution comes as (W_0, W_1) sol_player1_, sol_player2_ = strong_parity_solver_no_strategies(G_B) # depending on which player we are considering, assign regions to the proper variables # W''_j is noted W__j, sigma''_j is noted sig__j; the same aplies for jbar if j == 0: W__j = sol_player1_ W__jbar = sol_player2_ else: W__j = sol_player2_ W__jbar = sol_player1_ # the last step is to update the winning regions depending on which player we consider if j == 0: W1 = W__j W2.extend(W__jbar) W2.extend(B) else: W2 = W__j W1.extend(W__jbar) W1.extend(B) return W1, W2
def strong_parity_solver(g): """ Strong parity games solver. This is an implementation of the recursive algorithm used to solve parity games. :param g: the game to solve. :return: the solution in the following format : (W_0, sigma_0), (W_1, sigma_1). """ W1 = [] # Winning region of player 0 W2 = [] # Winning region of player 1 strat1 = defaultdict(lambda: -1) # Winning strategy of player 0 strat2 = defaultdict(lambda: -1) # Winning strategy of player 1 # if the game is empty, return the empty regions and strategies if len(g.nodes) == 0: return (W1, strat1), (W2, strat2) else: i = ops.max_priority(g) # get max priority occurring in g # determining which player we are considering, if i is even : player 0 and else player 1 if i % 2 == 0: j = 0 else: j = 1 opponent = ops.opponent(j) # getting the opponent of the player U = ops.i_priority_node(g, i) # target set for the attractor : nodes of priority i # getting the attractor A and the attractor strategy tau and discarding the region and strategy for the opponent (A, tau1), (discard1, discard2) = reachability.reachability_solver(g, U, j) # The subgame G\A is composed of the nodes not in the attractor, thus the nodes of the opposite player's region G_A = g.subgame(discard1) # Recursively solving the subgame G\A, solution comes as (W_0, sigma_0), (W_1, sigma_1) sol_player1, sol_player2 = strong_parity_solver(G_A) # depending on which player we are considering, assign regions and strategies to the proper variables # W'_j is noted W_j, sigma'_j is noted sig_j; the same aplies for jbar if j == 0: W_j, sig_j = sol_player1 W_jbar, sig_jbar = sol_player2 else: W_j, sig_j = sol_player2 W_jbar, sig_jbar = sol_player1 # if W'_jbar is empty we update the strategies and regions depending on the current player # the region and strategy for the whole game for one of the players is empty if not W_jbar: if j == 0: W1.extend(A) W1.extend(W_j) strat1.update(tau1) strat1.update(sig_j) else: W2.extend(A) W2.extend(W_j) strat2.update(tau1) strat2.update(sig_j) else: # compute attractor B and strategy nu (B, nu), (discard1, discard2) = reachability.reachability_solver(g, W_jbar, opponent) # The subgame G\B is composed of the nodes not in the attractor, so of the opposite player's winning region G_B = g.subgame(discard1) # recursively solve subgame G\B, solution comes as (W_0, sigma_0), (W_1, sigma_1) sol_player1_, sol_player2_ = strong_parity_solver(G_B) # depending on which player we are considering, assign regions and strategies to the proper variables # W''_j is noted W__j, sigma''_j is noted sig__j; the same aplies for jbar if j == 0: W__j, sig__j = sol_player1_ W__jbar, sig__jbar = sol_player2_ else: W__j, sig__j = sol_player2_ W__jbar, sig__jbar = sol_player1_ # the last step is to update the winning regions and strategies depending on which player we consider if j == 0: W1 = W__j strat1 = sig__j W2.extend(W__jbar) W2.extend(B) # nu is defined on W_jbar and must be replaced on W_jbar by the strategy sig_jbar # the replacement is implicit because updating sig_jbar last will overwrite already defined strategy strat2.update(nu) strat2.update(sig__jbar) strat2.update(sig_jbar) else: W2 = W__j strat2 = sig__j W1.extend(W__jbar) W1.extend(B) strat1.update(nu) strat1.update(sig__jbar) strat1.update(sig_jbar) return (W1, strat1), (W2, strat2)