def solve_fixwp(g, j, lam): """ Compute the winning state on the game g for the player j and the objective FixWP_j(lam). To do this, we will construct a new game g_new with the function build_buchi_game and we will compute the winning regions of a co-buchi objective on it (actually we compute a buchi objective in order to get the winning regions of the co-buchi objective). With that we will have the winning state of FixWP_j(lam). :param g: the game to solve. :param j: the player for who we want to retrieve the winning state of his objective. :param lam: the maximum size of a window. :return: a list containing the winning state of the player j. """ #construction of the game on which we will solve the buchi/co_buchi objective. g_new = build_buchi_game(g, j, lam) #The map and the lambda function take all the node in g and return a list with each the opposite of each element, that corresponds to the beta states beta = map(lambda x: x * -1, g.get_nodes()) #we compute the winning region of the player jbar for the objective Buchi(beta). w_buchi = buchi.basic_buchi(g_new, beta, ops.opponent(j)) #now we will transform what we just got into the winning region of the play j for the objective Buchi(S \ beta). #instead of basically computing the winning region of the co-buchi objective, we directly keep the state s that we are interested in. #because the true winning region of the co_buchi objective contains state in the following format : (s, c, l). w_cobuchi = [] for node_id in g_new.get_nodes(): #to avoid taking beta states. str are considered as > 0 if node_id > 0: s = g_new.nodes[node_id][1] if (node_id not in w_buchi) and (s not in w_cobuchi): w_cobuchi.append(s) #now we just have to return w_cobuchi because it is equal to the winning region of the player j for the objective FixWP_j(lam) return w_cobuchi
def solve_dirfixwp(g, j, lam): """ Compute the winning state on the game g for the player j and the objective DirFixWP_j(lam). To do this, we will construct a new game g_new with the function build_safety_game and we will compute the winning regions of a safety objective on it (actually we compute a reachability objective in order to get the winning regions of the safety objective). With that we will have the winning state of DirFixWP_j(lam). :param g: the game to solve. :param j: the player for who we want to retrieve the winning state of his objective. :param lam: the maximum size of a window. :return: a list containing the winning state of the player j. """ #construction of the game on which we will solve the safety/reachability objective. g_new = build_safety_game(g, j, lam) #we compute the winning region of the player jbar for the objective Reach(beta). w_reach = attr(g_new, [-1], ops.opponent(j))[0] #now we will transform what we just got into the winning region of the play j for the objective Reach(S \ beta). #instead of basically computing the winning region of the safety objective, we directly keep the state s that we are interested in. #because the true winning region of the safety objective contains state in the following format : (s, c, l). w_safety = [] for node_id in g_new.get_nodes(): if node_id != -1: s = g_new.nodes[node_id][1] if (node_id not in w_reach) and (s not in w_safety): w_safety.append(s) #now we just have to return w_safety because it is equal to the winning region of the player j for the objective DirFixWP_j(lam) return w_safety
def avoid_set_classical(g, t, j): """ Compute a set of states that are winning for player j for the buchi objective. :param g: the game to solve. :param t: the target set such that t is included in g's states. :return: a set of states that are winning for player j for the buchi objective. """ jbar = ops.opponent(j) a, abar = attr(g, t, j) #O(n+m) w_current = attr(g, abar, jbar)[0] #O(n+m) #the total is in O(2n+2m+n^2) = O(n^2) return w_current
def attractor(g, U, j): """ Computes the attractor for player j of the set U in g. Does not create any strategy and only returns the set that corresponds to the attractor. :param g: the game graph. :param U: the target set. :param j: the player for which we compute the attractor. :return: W the set of nodes corresponding to the attractor. """ out = init_out(g) # init out queue = deque() # init queue (deque is part of standard library and allows O(1) append() and pop() at either end) # this dictionary is used to know if a node belongs to a winning region without # iterating over both winning regions lists (we can check in O(1) in average) regions = defaultdict(lambda: -1) W = [] # the attractor opponent = op.opponent(j) # player j's opponent # for each node in the target set U for node in U: queue.append(node) # add node to the end of the queue regions[node] = j # set its regions to j (node is winning for j because reachability objective is satisfied) W.append(node) # add the node to the winning region list of j # while queue is not empty while queue: s = queue.popleft() # remove and return node on the left side of the queue (first in, first out) # iterating over the predecessors of node s for sbis in g.get_predecessors(s): if regions[sbis] == -1: # if sbis is not yet visited, its region is -1 by default if g.get_node_player(sbis) == j: # belongs to j, set regions and strategy accordingly queue.append(sbis) regions[sbis] = j W.append(sbis) elif g.get_node_player(sbis) == opponent: # belongs to j bar, decrement out. If out is 0, set the region accordingly out[sbis] -= 1 if out[sbis] == 0: queue.append(sbis) regions[sbis] = j W.append(sbis) Wbis = [] for node in g.get_nodes(): if regions[node] != j: Wbis.append(node) return W, Wbis
def new_buchi(g, t, j): """ Solve a Buchi objective on the game g for the target set t. This version of the algorithm works in O(n^2) where n is the number of states in g. This algorithm returns the winning state for player j only. If we want to we can get the winning states for player jbar by taking S \ Wj. :param g: the game to solve. :param t: the target set. :return: the winning regions of player j for the objective Buchi(g, t). """ #first loop index, we are iterating on k and i. Not directly but those two number change and so are the rest of the data k = 0 #y is the attractor in g for player j towards the set t # x = s \ y so the states that are not in the attractor y, x = attr(g, t, j) #O(m+n) #d the set of states that we removed from the current game g_k #s_k = s \ d, so the states that are not in the attractor d, s_k = attr(g, x, ops.opponent(j)) #O(n+m) #we have to sort the edge of g_k such that edge (u, v) are first with u such that u belongs to jbar and u is not in t #we do it only one time, once it's sorted it will stay sorted with the way subgames are created for v in g.get_nodes(): #O(m) first_succ = [] last_succ = [] for u in g.get_predecessors(v): if g.get_node_player(u) == ops.opponent(j) and u not in t: first_succ.append(u) else: last_succ.append(u) g.predecessors[v] = first_succ + last_succ #the current game g_k g_k = g.subgame(s_k) #target set in iteration k to correspond with the remaining state of g_k t_k = copy.deepcopy(t) #updating the iteration we are on k = k + 1 #set of all the states we removed from g from that start U = d #second loop index i = 1 #"repeat until" equivalent. We repeat the loop until we have i = log_2(n) while True: #O(log(n)) #construct a special graph with and ordering on inedges and color on states g_i_k = construct_g_i_k(g_k, i, k, j) #O(n*m) #compute z_i_k to be a set containing states from g_i_k such that they are (i) red (color 1) without outedges in g_i_k or (ii) blue (color 0) in g_i_k z = [] for s in g_i_k.get_nodes(): #O(n) s_color = g_i_k.get_node_priority(s) if (s_color == 1 and len(g_i_k.get_successors(s)) == 0) or s_color == 0: z.append(s) #actualing the target set to correspond with the current set of state t_k = [s for s in t_k if s in s_k] #O(n^2) target = t_k + z #computing y with our new value #y is the attractor in g for player j towards the set t # x = s \ y so the states that are not in the attractor y, x = attr(g, target, j) #O(m+n) #incrementing i i = i + 1 #if x is not empty we compute a new d and remove it from the g_k if len(x) != 0: #d the set of states that we removed from the current game g_k #s_k = s \ d, so the states that are not in the attractor d, s_k = attr(g, x, ops.opponent(j)) #O(n+m) g_k = g_k.subgame(s_k) k = k + 1 U = U + d #we stop when i = log_2(n) if len(g.get_nodes()) == 0 or i >= np.log2(len(g.get_nodes())): break #we will return all states but those who were removed ie we return the set of states S \ U Wj = [s for s in g.get_nodes() if s not in U] #O(n^2) return Wj
def reachability_solver(g, U, j): """ Reachability games solver. This function computes Att_j^g(U), the attractor for player j of target set U in the game g. That attractor is the winning region of player j who has the reachability objective in the game. The rest of the nodes are part of the winning region of player jbar (player j's opponent). Winning regions and strategies are computed and returned by the algorithm. The winning regions and strategies are return as two tuples to resemble pseudo-code and facilitate weak and strong parity solvers readability. :param g: the game graph. :param U: the target set. :param j: the player with the reachability objective. :return: two tuples : (w_j, strat_j), (w_jbar, strat_jbar) where w_j and w_jbar are lists containing nodes of their respective winning regions and where strat_j and strat_jbar are dictionaries containing winning strategies. """ out = init_out(g) # init out queue = deque() # init queue (deque is part of standard library and allows O(1) append() and pop() at either end) # this dictionary is used to know if a node belongs to a winning region without # iterating over both winning regions lists (we can check in O(1) in average) regions = defaultdict(lambda: -1) region_j = [] # winning region of j region_opponent = [] # winning region of j bar strat_j = defaultdict(lambda: -1) # init strat for player j strat_opponent = defaultdict(lambda: -1) # init strat for player jbar opponent = op.opponent(j) # player j's opponent (jbar) # for each node in the target set U for node in U: queue.append(node) # add node to the end of the queue regions[node] = j # set its regions to j (node is winning for j because reachability objective is satisfied) region_j.append(node) # add the node to the winning region list of j # if node belongs to j, set an arbitrary strategy for that node (we chose to select first successor) if g.get_node_player(node) == j: strat_j[node] = g.get_successors(node)[0] # while queue is not empty while queue: s = queue.popleft() # remove and return node on the left side of the queue (first in, first out) # iterating over the predecessors of node s for sbis in g.get_predecessors(s): if regions[sbis] == -1: # if sbis is not yet visited, its region is -1 by default if g.get_node_player(sbis) == j: # belongs to j, set regions and strategy accordingly queue.append(sbis) regions[sbis] = j region_j.append(sbis) strat_j[sbis] = s elif g.get_node_player(sbis) == opponent: # belongs to j bar, decrement out. If out is 0, set the region accordingly out[sbis] -= 1 if out[sbis] == 0: queue.append(sbis) regions[sbis] = j region_j.append(sbis) # for each node that is not marked we set its region to the opponent and find a successor for the strategy for node in g.get_nodes(): if regions[node] != j: regions[node] = opponent region_opponent.append(node) if g.get_node_player(node) == opponent: for successor in g.get_successors(node): if regions[successor] != j: strat_opponent[node] = successor return (region_j, strat_j), (region_opponent, strat_opponent)
def strong_parity_solver_non_removed(g, removed): """ Strong parity games solver. This algorithm is an implementation of the recursive algorithm used to solve parity games. It uses a list of non-removed nodes as a way to track sub-games. The attractor computation also uses this technique. The value at position i in the list is true if node i is removed from the original game arena. :param removed: the removed nodes. :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 # removed is a bitarray, count(42) counts the occurrences of True # if every element in the list is true, every node is removed and the game is empty if removed.count(42) == len(g.nodes): return (W1, strat1), (W2, strat2) else: i = ops.max_priority_non_removed(g, removed) # get max priority occurring in g, considering the removed nodes # 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 # target set for the attractor : nodes of priority i, considering the removed nodes U = ops.i_priority_node_non_removed(g, i, removed) # getting the attractor A and the attractor strategy tau and discarding the region and strategy for the opponent # using the attractor function which considers the non removed nodes (A, tau1), (discard1, discard2) = reachability.reachability_solver_non_removed(g, U, j, removed) # The subgame G\A is composed of the nodes not in the attractor, thus the nodes of the opposite player's region # Copy the bitarray and remove the nodes of the attractor by setting their value to True in the list copy_removed1 = bitarray(removed) for nodes in A: copy_removed1[nodes] = True # Recursively solving the subgame G\A, solution comes as (W_0, sigma_0), (W_1, sigma_1) sol_player1, sol_player2 = strong_parity_solver_non_removed(g, copy_removed1) # 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_non_removed(g, W_jbar, opponent, removed) # The subgame G\B is composed of the nodes not in the attractor, so of the opposite player's winning region copy_removed2 = bitarray(removed) for nodes in B: copy_removed2[nodes] = True # recursively solve subgame G\B, solution comes as (W_0, sigma_0), (W_1, sigma_1) sol_player1_, sol_player2_ = strong_parity_solver_non_removed(g, copy_removed2) # 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)
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)