{"slug": "invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling", "title": "Invariant Coordinates for Normal-Form Games Modulo Strategy Relabeling", "summary": "Researchers have identified 17 invariant coordinates for normal-form games under strategy relabeling, consisting of nine degree-2 and eight degree-3 polynomial generators. The discovery provides a complete algebraic description of game payoffs modulo row and column permutations, enabling systematic analysis of game symmetries. This invariant coordinate system allows researchers to classify and compare games independent of player labeling, with applications in game theory and evolutionary dynamics.", "body_md": "\n\n``` python\nimport numpy as np\nfrom itertools import combinations_with_replacement\nfrom sympy import symbols, expand\n\nrA, cA, dA, rB, cB, dB = symbols('rA cA dA rB cB dB')\nVARS = [rA, cA, dA, rB, cB, dB]\n\ndef build_s2xs2():\n    \"\"\"S_2 x S_2 acting on (rA, cA, dA, rB, cB, dB) by row/col sign flips.\"\"\"\n    I = np.eye(6)\n    s1 = np.diag([-1, 1, -1, -1, 1, -1.0])  # row swap\n    s2 = np.diag([1, -1, -1, 1, -1, -1.0])  # col swap\n    return [I, s1, s2, s1 @ s2]\n\ndef reynolds_symbolic(exp_vec, group):\n    total = 0\n    for g in group:\n        gv = [sum(int(round(g[i, j])) * VARS[j] for j in range(6)) for i in range(6)]\n        mono = 1\n        for k, e in enumerate(exp_vec):\n            mono *= gv[k] ** e\n        total += mono\n    return expand(total / len(group))\n\ndef reynolds_numerical(exp_vec, group, points):\n    n = len(points)\n    vals = np.zeros(n)\n    for g in group:\n        for j in range(n):\n            gpt = g @ points[j]\n            v = 1.0\n            for k, e in enumerate(exp_vec):\n                v *= gpt[k] ** e\n            vals[j] += v\n    return vals / len(group)\n\ndef find_all_generators(group, n_pts=400, seed=42, max_deg=4):\n    \"\"\"Find generators up to Noether bound = |G| = 4.\"\"\"\n    rng = np.random.default_rng(seed)\n    pts = 2 * rng.standard_normal((n_pts, 6))\n    all_num, all_sym, all_deg = [], [], []\n    molien = {}\n\n    for deg in range(max_deg + 1):\n        mono_list = []\n        for combo in combinations_with_replacement(range(6), deg):\n            exp = [0] * 6\n            for c in combo:\n                exp[c] += 1\n            if tuple(exp) not in mono_list:\n                mono_list.append(tuple(exp))\n        if not mono_list:\n            mono_list = [(0,) * 6]\n\n        prods = []\n        for i in range(len(all_num)):\n            for j in range(i, len(all_num)):\n                if all_deg[i] + all_deg[j] == deg:\n                    prods.append(all_num[i] * all_num[j])\n            for j in range(i, len(all_num)):\n                for k in range(j, len(all_num)):\n                    if all_deg[i] + all_deg[j] + all_deg[k] == deg:\n                        prods.append(all_num[i] * all_num[j] * all_num[k])\n\n        prod_mat = np.array(prods) if prods else np.zeros((0, n_pts))\n        current = prod_mat.copy() if len(prods) > 0 else np.zeros((0, n_pts))\n        current_rank = np.linalg.matrix_rank(current, tol=1e-8) if current.shape[0] else 0\n\n        for m in mono_list:\n            rv = reynolds_numerical(m, group, pts)\n            test = rv.reshape(1, -1) if current.shape[0] == 0 else np.vstack([current, rv.reshape(1, -1)])\n            r = np.linalg.matrix_rank(test, tol=1e-8)\n            if r > current_rank:\n                all_num.append(rv)\n                all_sym.append(reynolds_symbolic(m, group))\n                all_deg.append(deg)\n                current, current_rank = test, r\n\n        molien[deg] = current_rank\n    return all_sym, all_deg, molien\n\ndef eval_generators(rA, cA, dA, rB, cB, dB):\n    \"\"\"The 17 generators: 9 degree-2 (squares and cross-products), 8 degree-3 triples.\"\"\"\n    deg2 = [rA**2, rA*rB, cA**2, cA*cB, dA**2, dA*dB, rB**2, cB** 2, dB**2]\n    deg3 = [cA*dA*rA, cA*dB*rA, cB*dA*rA, cB*dB*rA,\n            cA*dA*rB, cA*dB*rB, cB*dA*rB, cB*dB*rB]\n    return deg2 + deg3\n\nif __name__ == '__main__':\n    G = build_s2xs2()\n    gens_sym, gens_deg, molien = find_all_generators(G)\n    print(f\"Total generators: {len(gens_sym)}; Molien: {[molien[d] for d in range(5)]}\")\n    for i, (expr, deg) in enumerate(zip(gens_sym, gens_deg)):\n        print(f\"  g{i} (deg {deg}) = {expr}\")\n\n    rng = np.random.default_rng(42)\n    pts = 2 * rng.standard_normal((400, 6))\n    gen_vals = np.array([eval_generators(*pt) for pt in pts])\n    for check_deg in [4, 5, 6]:\n        prods = []\n        for i in range(17):\n            di = 2 if i < 9 else 3\n            for j in range(i, 17):\n                dj = 2 if j < 9 else 3\n                if di + dj == check_deg:\n                    prods.append(gen_vals[:, i] * gen_vals[:, j])\n                for k in range(j, 17):\n                    dk = 2 if k < 9 else 3\n                    if di + dj + dk == check_deg:\n                        prods.append(gen_vals[:, i] * gen_vals[:, j] * gen_vals[:, k])\n        rank = np.linalg.matrix_rank(np.array(prods), tol=1e-8)\n        expected = {4: 42, 5: 48, 6: 138}[check_deg]\n        print(f\"Closure deg {check_deg}: rank={rank}, Molien={expected}\")\n```\n\n**Draft Note**: This is the first draft of a paper I will be potentially be revising (probably substantially), reformatting, and submitting to ArXiv in the near future, and as such is written in that style. Note that it is not yet reviewed/fully checked, and may contain errors or incomplete arguments. I expect it to undergo significant revisions. I [welcome](https://demonstrandom.com/contact.html) feedback and suggestions for improvement. I’ll also note that the appications of this framework will be explored in future work, and are not the focus of this post. This effort is a more sophisticated attempt to classify games that I first attempted [here](https://demonstrandom.com/game_theory/posts/canonical_games/index.html).\n\nGiven a game, how can we tell what kind of game it is? And when are two games “the same”? Consider a finite normal-form game with players and strategies per player (which we denote as an “-game”). Each finite normal-form game can be represented by a multidimensional array of payoffs in . Ideally, an equivalence relation on games would identify games that differ only by arbitrary labeling choices of strategies, while preserving strategic distinctions such as dominance, coordination, cycling, and equilibrium structure.\n\nWe use computational invariant theory to classify finite normal-form games modulo strategy relabeling by constructing invariant coordinates on the quotient. The relabeling action is linear on the space of payoff arrays, and polynomial invariants of this action are the payoff statistics that are independent of the names assigned to strategies. Thus, finite generating sets of the invariant ring give orbit-separating coordinates for games modulo relabeling.\n\nWe apply this framework first to -games, where we compute an explicit generating set of invariants, together with relations among them, that completely classify -games up to relabeling. We show that this generalizes the Robinson and Goforth (2005) combinatorial taxonomy of -games under ordinal equivalence to cardinal payoff geometry, with the Robinson-Goforth types appearing as regions in the resulting quotient. We also show that by using an enlarged notion of equivalence that also identifies player swaps, we can generalize the Rapoport and Guyer (1966) classification of -games under strict ordinal equivalence.\n\nWe then generalize the construction to finite -games. For each fixed , the resulting invariant ring separates strategy-relabeling orbits, giving an invariant-theoretic classification of -games. In practice, this classification is realized by computing finite generating sets of polynomial invariants and the relations among them. Beyond the complete explicit case, we develop the computational pipeline needed for larger cases, including the setting, where the invariant ring is substantially larger but governed by the same quotient construction.\n\nWe go on to show that standard game classes (potential, zero-sum, symmetric, coordination-type) can be characterized by polynomial equations and inequalities in the invariants. We record scaling laws for the invariant ring, including stabilization of once and a closed-form super-polynomial growth rate for binary degree- invariants. We also describe how label-complete cyclic witnesses produce natural degree- invariants and sign-reversing pairs produce degree- invariants, while emphasizing that these are existence statements rather than universal lower bounds.\n\nFinally, we relate the quotient coordinates to the Candogan et al. (2011) Hodge decomposition of games, and show how specific invariants detect dominance and Nash-equilibrium structure. For two-player games (n=2), the full-support indifference determinant is a payoff-only invariant polynomial of degree . For , the natural object is a Jacobian form on the payoff–mixed-strategy incidence space, polynomial in payoff entries of degree .\n\nMany of the common -games have familiar names. The Prisoner’s Dilemma, Stag Hunt, Chicken, Battle of the Sexes, Matching Pennies, etc. are standard examples of -games, each modeling a distinct class of strategic interactions. But these named examples occupy only a small part of the full space of finite games.\n\nIn this paper, we construct a coordinate system on the space of games. The coordinates respect the arbitrary labeling of strategies, retain full cardinal payoff information, and do not depend on any solution concept. We show that this coordinate system recovers the classical game classes as algebraic subvarieties and inequalities, encodes Nash equilibrium structure as polynomial conditions, and reveals a hierarchy of strategic properties organized by polynomial degree.\n\nThe first systematic classification of the -games was Rapoport and Guyer (1966). Rapoport and Guyer identify 78 types of games using a strict ordinal framework, where two games are considered equivalent if and only if they have the same payoff ranking structure. They also considered games to be identical under relabelings of rows, columns, and player positions. Robinson and Goforth (2005) later obtained 144 types by distinguishing player roles. This allowed them to organize the resulting space of -games topologically (via adjacencies given by single payoff swaps), producing a “periodic table”.\n\nWhile these ordinal classification methods produce finite taxonomies, they also collapse the cardinal geometry within each type. Two games may have the same ordinal form while differing in mixed equilibrium probabilities, risk dominance, evolutionary behavior, or comparative statics. At the same time, small cardinal perturbations near an ordinal boundary can move a game into a different ordinal type.\n\nThe situation is worse in higher dimensions. Consider -games, with two players and three strategies per player. Although a few examples, such as Rock-Paper-Scissors, are well known, most -games have no standard names and no useful catalog. In fact, the number of strict ordinal types of -games exceeds .1 Since the number of strict ordinal types grows superexponentially in the number of strategies, a Robinson-Goforth-style classification based on exhaustive enumeration of ordinal types is infeasible.\n\nOrdinal classification is not the only possible approach. Another approach classifies games by their induced behavior. For example, two games can be considered equivalent if they generate the same best-response correspondence (Morris and Ui 2004), the same Nash equilibrium structure (Germano 2006), or the same qualitative dynamics (Weibull 1995). However, each such equivalence is tied to a specific solution concept. For example, games that behave identically under best-response dynamics might differ under other solution concepts (e.g., welfare, fictitious play, replicator dynamics).\n\nWe therefore want a classification that respects relabeling symmetries, retains cardinal payoff information, and does not presuppose a solution concept. Such a classification would let us study the algebraic structure of the space of games, identify canonical coordinates, and describe game classes and equilibrium structure systematically.\n\nIn this paper, we use invariant theory to build such a coordinate system for the space of games. In the main part of the paper, games are considered equivalent if they differ only by relabeling of strategies. Equivalently, we consider two games to be equivalent if they lie in the same orbit under the action of the permutation group acting on the labels of the strategies. The invariants of this group action are polynomial functions of the payoff entries that are constant on orbits, meaning they do not change when we relabel strategies. These invariants act as canonical summary statistics of a game, independent of labeling conventions. Together, the invariants give coordinates on the space of games modulo relabeling.\n\nSince the invariants are polynomials in the actual payoff values, cardinal information is retained by construction. Additionally, the equivalence relation is defined by labeling symmetries without reference to any solution concept. We go on to show that the invariant ring has a rich algebraic structure that captures game-theoretic properties directly. For example, classical game classes such as potential, zero-sum, and coordination games can be described by explicit polynomial equations and inequalities. Dominance and Nash-equilibrium structure are encoded by polynomial conditions on the invariants. The invariant degrees at which various strategic properties first become detectable form a hierarchy, with contrast magnitudes and interaction alignment appearing at degree 2, skewness and cyclic directionality at degree 3, and higher-order cyclic patterns at degree 4 and above. The Hodge-theoretic decomposition of games into potential, harmonic, and nonstrategic components (Candogan et al. 2011; Candogan, Ozdaglar, and Parrilo 2013) sits inside the invariant ring as a relabeling-compatible linear decomposition of the payoff space, with the invariant polynomials then organized by which Hodge components they involve.\n\nThe invariant ring also connects cleanly to existing behavioral classifications. Best-response equivalence, Nash equivalence, and other solution-concept-based equivalences each can be studied inside the relabeling quotient, and they appear in invariant coordinates as polynomial conditions where they appear as additional equations and inequalities in invariant coordinates. The invariant ring is thus not a replacement for these classifications but an underlying geometry in which they can be located and compared.\n\nThe rest of the paper is organized as follows. We begin with background on game theory, symmetries, and invariant theory. We then apply the framework to -games, computing explicit generators, syzygies, game class conditions, and the connection to the Robinson-Goforth ordinal classification. We generalize to -games via the family structure and the contrast-block Reynolds construction. We then collect applications and further structure: scaling laws for the invariant ring, equilibrium diagnostics, the relation to the Hodge decomposition, and cycle-witness invariants. We conclude with algorithms, a discussion of open problems, and appendices containing the wreath product computation, the -game details, and software documentation.\n\nWe develop a computational invariant-theoretic framework for finite normal-form -games under strategy relabeling. Applications are deferred to follow-up work. The setting is finite normal-form games, so extensive-form representations and games with infinite strategy spaces are not considered. The equivalence relation is purely structural, namely strategy relabeling, with no appeal to a solution concept. Behavioral equivalences such as best-response, Nash, and qualitative-dynamics equivalence sit inside the relabeling quotient as polynomial conditions on the invariants, but constructing them from solution concepts is not pursued, and the learning, evolutionary, and mechanism-design questions that the framework is designed to support are also out of scope for this paper. We also do not aim to catalog the full invariant rings at every (which would be impossible). The contribution of this paper is in showing how these methods can be applied to games, such as the contrast-block decomposition and the Reynolds projection from standard invariant theory, which compute the ring on demand at any specific .\n\nLet be a set of players. For each player , let be a finite strategy set with , and let be the payoff function for player . We call the elements of strategy profiles, and the payoff to player at profile . The complete tuple is called a finite normal-form game. If and every player has strategies, we call the game an -game.\n\nA general finite normal-form game has payoff entries, and an -game has payoff entries. Since the payoff functions determine the game once the player and strategy sets are fixed, the space of -games can be identified with .\n\nWhen and both players have strategies, we can write the game as a pair of matrices , where is the payoff to player 1 and is the payoff to player 2 at the strategy profile . For , the payoff functions are arrays indexed by . We will occasionally return to the two-player matrix notation for concreteness and visualization.\n\nGiven a strategy profile , we write for the strategies of all players other than , and for the full profile.\n\nWe call a probability distribution over a mixed strategy for player . When , we write\n\nfor the -simplex. Thus, a mixed strategy is an element . For a two-player game with mixed strategies , the expected payoffs are and (Neumann and Morgenstern 1944; Nash 1950).\n\nWe call the set of strategies maximizing the best response of player to . We call a profile of mixed strategies a Nash equilibrium if each is a best response to . Nash (1950) showed that every finite game has at least one Nash equilibrium.\n\nWe say that a strategy for player is strictly dominated if there exists such that for all . Dominated strategies are never played at any Nash equilibrium (Osborne and Rubinstein 1994). We say that a game is solvable by iterated strict dominance if iteratively eliminating strictly dominated strategies terminates at a single strategy profile.\n\nSeveral well-known classes of games are defined by equations or inequalities on the payoff functions.\n\nWe say that a game is zero-sum if for all . Similarly, a game is constant-sum if is constant across all strategy profiles . Constant-sum games are therefore strategically equivalent to zero-sum games after a payoff shift. With the convention that and are the two payoffs at the same profile , a two-player game is zero-sum if and only if (Neumann and Morgenstern 1944).\n\nWe call a game an exact potential game (Monderer and Shapley 1996) if there exists a function such that for each player , each strategy , and each alternative strategy , with held fixed,\n\nThus every unilateral payoff improvement produces the same change in . Pure Nash equilibria are the specific strategy profiles that are local maxima of with respect to unilateral deviations.\n\nWe say that an -game is symmetric if permuting the players does not change any player’s payoff, provided the strategies are permuted accordingly. For two-player games with equal strategy sets, this is equivalent to .\n\nWe will also use the informal terms “coordination-type” and “anti-coordination-type.” In a coordination-type game, players benefit from matching each other’s strategies, whereas in an anti-coordination-type game, players benefit from choosing different strategies. The Stag Hunt and Prisoner’s Dilemma are coordination-type; Chicken and Matching Pennies are anti-coordination-type. In the invariant coordinates below, these distinctions will correspond to explicit sign conditions.\n\nWe illustrate with named -games. A general -game has 8 payoff entries:\n\nwhere is the payoff to player 1 and the payoff to player 2. We refer to the outcome with payoffs as . The payoff space is with coordinates .\n\nTable 1 summarizes the named -games used in this paper.\n\n| Game | P1 ranking | P2 ranking | Class | NE |\n|---|---|---|---|---|\n| Prisoner’s Dilemma | 3, 1, 4, 2 | 2, 1, 4, 3 | potential | unique |\n| Stag Hunt | 1, 3, 4, 2 | 1, 2, 4, 3 | potential | and |\n| Chicken | 3, 1, 2, 4 | 2, 1, 3, 4 | anti-coordination | and |\n| Pure Coordination | 1, 4 (others 0) | 1, 4 (others 0) | coordination | and |\n| Matching Pennies | (BR cycle) | zero-sum | fully mixed |\n\nThe inequalities in Table 1 specify ordinal representatives of the named classes. Later invariant calculations use particular cardinal representatives.\n\nIn the Prisoner’s Dilemma, strategy 2 strictly dominates strategy 1 for both players, producing the unique equilibrium despite being Pareto superior. In the Stag Hunt, changing one inequality relative to the PD creates two equilibria: is payoff-dominant but is risk-dominant. In Chicken, each player prefers to differ from the other, giving two asymmetric pure equilibria. Pure Coordination is the extreme case where off-diagonal payoffs are zero. In Matching Pennies, the zero-sum condition creates a best-response cycle with no pure equilibrium.\n\nRock-Paper-Scissors (RPS) extends the cycling structure of Matching Pennies to . The standard RPS game is a zero-sum game with and , where each strategy beats one strategy and loses to another. The unique Nash equilibrium is the uniform mixture .\n\nThe named examples above are highly structured. The standard symmetric examples satisfy , while Matching Pennies is zero-sum with . A generic game in has and unrelated. Thus the standard symmetric and zero-sum representatives of named games occupy lower-dimensional subsets of payoff space, even though the corresponding ordinal classes may contain open regions.\n\nGiven a game, we can relabel the strategies of each player without changing the strategic structure of the game. For example, in a two-player game, we can swap the labels of player 1’s strategies (e.g., “cooperate” and “defect”) or swap the labels of player 2’s strategies. Simply renaming strategies does not change the underlying strategic interaction, only the labels used to describe it. Therefore, we consider two games to be equivalent if they can be transformed into each other by such relabeling operations.\n\nWe can formalize this idea using group theory. A permutation of player ’s strategies is an element of the symmetric group , which acts on the payoff array by permuting the corresponding indices. A permutation of the players is an element of the symmetric group , which acts by permuting the player indices. The combined action of these permutations generates a group of symmetries that we can use to classify games up to relabeling.\n\nA group is a set equipped with a multiplication operation, an identity element, and inverses, satisfying the usual associativity law. The basic examples in this paper are permutation groups, especially , the group of permutations of objects.\n\nA group action of on a set assigns to each a transformation of , written , such that the identity element fixes every and\n\nThe orbit of is\n\nThe orbits partition into equivalence classes. The quotient is the set of these orbits, and the quotient map sends each element to its orbit . There may not in general be a natural way to identify with a subset of , and the quotient space may also have a different geometric or algebraic structure than the original space.\n\nPermutations form a group, called the “symmetric group”, commonly denoted for permutations of distinct objects. itself consists of different elements. For example, has 6 elements: the identity permutation, the three transpositions that swap two elements, and the two 3-cycles that rotate all three elements. Permutations can be composed in the expected way (permuting the items, then permuting the permuted items), and the group operation is associative. The identity element is the permutation that leaves all elements unchanged, and each permutation has an inverse that undoes its effect.\n\nWe write permutations in cycle notation: denotes the transposition swapping and , while denotes the cycle .\n\nWe can also represent permutations by permutation matrices. Let be the standard basis of . For , define by\n\nThus the -th column of is , and multiplying by permutes coordinates according to .\n\nFor each player , a permutation relabels player ’s strategies. Since every payoff array is indexed by the same strategy profile , the permutation acts on every payoff array by permuting the -th index:\n\nThus the same relabeling of player ’s strategies is applied simultaneously to every player’s payoff array. The inverse appears because the action is written as a left action on payoff functions.\n\nFor two-player games written as , this action becomes matrix multiplication. With the convention , a permutation of player 1’s strategies acts by\n\nand a permutation of player 2’s strategies acts by\n\nLeft multiplication by permutes rows; right multiplication by permutes columns. Both and are permuted in the same way, since both payoff matrices are indexed by the same strategy profiles.\n\nFor a -game with , the nontrivial element of has permutation matrix . Then\n\nThe first swaps rows (relabeling player 1’s strategies), the second swaps columns (relabeling player 2’s strategies). Both act on the same way.\n\nFor an -game, each payoff array is indexed by . The nontrivial element of the -th copy of flips the -th coordinate in every player’s payoff array.\n\nFor an -game, each player has an independent copy of acting on that player’s strategies. Since these relabelings can be chosen independently for each player, the combined strategy-relabeling group is the direct product\n\nAn element relabels all players’ strategies simultaneously. The factor relabels player ’s strategies, and every payoff array is permuted in the corresponding strategy coordinate. This is the main symmetry group uses in this paper.\n\nThe players remain distinguishable: relabeling player 1’s strategies is a different operation from relabeling player 2’s, and we do not identify the two players. In settings where players are interchangeable, such as anonymous mechanism design or evolutionary models, one can also allow permutations of the players by .\n\nWhen the players are interchangeable (as in anonymous mechanism design or evolutionary settings), we can additionally permute the players themselves by . The resulting group is the wreath product\n\nwhich we treat separately in Appendix C.\n\nWe now review the elements of invariant theory needed for the classification. Standard references are Derksen and Kemper (2015) and Sturmfels (2008). Throughout, is a finite group acting linearly on a finite-dimensional real vector space .\n\nA linear action of on is a group homomorphism . The orbit of a point is the set , and the stabilizer of is . Two points lie in the same orbit if and only if one can be obtained from the other by applying some group element.\n\nLet denote the ring of real-valued polynomial functions on . Since degree polynomials form a subspace , the ring is graded by polynomial degree. The group action preserves degree, and therefore the invariant ring inherits this grading. A polynomial is -invariant if for all and . The set of all -invariant polynomials forms a subring\n\nwhere\n\ncalled the invariant ring.\n\nFor finite groups, invariants can be constructed explicitly by averaging. The Reynolds operator is defined as\n\nThis map is a linear projection from onto . The Reynolds operator sends every polynomial to an invariant polynomial, and maps any polynomial that is already invariant back to itself. In practice, to find degree- invariants, one applies to a basis of degree- monomials and collects the linearly independent results.\n\nLet act on by swapping coordinates: .\n\nThen is the ring of symmetric polynomials, generated by and . These two generators are algebraically independent, so is a polynomial ring. Every symmetric polynomial in and can be written uniquely as a polynomial in and . For example, the symmetric polynomial can be expressed in terms of the generators as .\n\nBy the Hilbert–Noether finite-generation theorem for invariant rings (Hilbert 1890; Noether 1926), the invariant ring is finitely generated as an -algebra. That is, there exist finitely many invariants such that every invariant polynomial can be written as a polynomial in . We call a set of generators for the invariant ring.\n\nIn the symmetric polynomial example above, the generators are algebraically independent: there is no nontrivial polynomial relation satisfied by the generators. In general, generators need not be algebraically independent.\n\nGiven generators , we can consider the surjection defined by . This map sends each abstract polynomial in the to the corresponding polynomial in the generators, evaluated on . The kernel of is the ideal of all polynomial relations among the generators that hold identically on . We call these relations, or “syzygies”, among the generators. For example, if as polynomial functions on , then is a syzygy.\n\nTogether, the generators and syzygies give a complete algebraic description of the invariant ring. Specifically, the generators define coordinates on the quotient, and the relations cut out the image of the quotient map inside .\n\nThe Molien series is a generating function that counts the dimension of the space of invariants at each polynomial degree. For a finite group acting on via the representation , the Molien series is defined as\n\nwhere is the number of linearly independent degree- invariants. The formula follows from Molien’s theorem (see Derksen and Kemper 2015, Ch. 3). In practice, the sum over group elements can often be reduced to a sum over conjugacy classes, since depends only on the conjugacy class of .\n\nThe Molien series does not describe the invariants themselves. Instead, it gives the dimension of the invariant space in each degree. By comparing with the span of products of lower-degree generators, we can determine when new generators are needed and check explicit computations.\n\nThe generators define a map\n\nThe image of this map, denoted , realizes the quotient of by the action of . For finite groups , points of this quotient correspond to orbits. Two points lie in the same orbit if and only if , i.e., if and only if they take the same values on all generators. Equivalently, the generators separate orbits: satisfy if and only if for all .\n\nFor finite groups, the invariant ring separates orbits because all orbits are finite, hence closed. The distinction between orbits and closed orbits in general invariant theory will not be necessary here.\n\nBefore developing the general construction, we will work out the smallest nontrivial case in full, which is the invariant ring of -games under strategy relabeling by the action of the symmetry group , where the two factors swap the two strategies of player 1 and player 2, respectively. We assume players are distinguishable to match the assumptions of the Robinson-Goforth 144-type ordinal classification.\n\nThe case simplifies the case in two convenient ways. First, the relabeling action diagonalizes into simple sign flips. Second, the resulting invariants can be written explicitly. This example therefore serves as a concrete model for the general quotient construction, while also allowing direct comparison with the Robinson-Goforth ordinal taxonomy.\n\nWe will show that the invariant ring can be used to construct a cardinal analogue of Robinson-Goforth. Starting from the eight payoff entries of a -game, we will remove the two payoff means and work on the six-dimensional mean-zero payoff space. In these coordinates, the two strategy swaps act by changing signs of selected coordinates, reducing the computation of invariants to a parity condition (i.e. checking the sign) on monomials. The invariant ring is generated by nine quadratic invariants and eight cubic invariants. These real-valued invariants classify cardinal -games up to strategy relabeling and player-specific additive constants, subject to algebraic relations among the generators. By discarding magnitudes and keeping only signs of nine selected degree-2 invariants, we recover the Robinson-Goforth taxonomy from this quotient. Thus, the same invariant coordinates retain cardinal payoff information while also explaining how the classical ordinal table sits inside the quotient.\n\nWe first pass to mean-zero coordinates, removing the two player-specific payoff means. Consider a two-player game with payoff matrices and . We write player 1’s payoff matrix as\n\nBased on player 1’s payoff entries , we define\n\nand similarly for player 2 with entries in . We call the row contrast2, the column contrast, and the interaction. The words “row” and “column” refer to the payoff table coordinates. Thus is player 1’s own-strategy contrast, while is player 2’s own-strategy contrast.\n\nThe remaining coordinate for player 1 is the payoff sum\n\nand similarly for player 2.\n\nThe inverse change of coordinates for player 1 is\n\nThe same inverse formula holds for .\n\nAdding a constant to all of one player’s payoffs does not affect best responses or Nash equilibria, so we suppress these two coordinates. Together, give linear coordinates on the six-dimensional mean-zero payoff space.\n\nWe use unnormalized contrast coordinates, omitting the conventional scaling factor. This keeps all invariant values integral on games with integer payoffs. The choice of scaling does not affect the invariant ring.\n\n**Proposition 1 (Sign-flip action)** In the mean-zero coordinates , the group acts by the following two sign-flip generators:\n\n*Proof*. Let . Swapping player 1’s strategies swaps the two rows, sending . Under this swap,\n\nThe same row swap acts on player 2’s payoff matrix in the same way, so and are negated while is fixed. This gives the formula for .\n\nSimilarly, swapping player 2’s strategies swaps the two columns, sending . A direct substitution gives , , . The same column swap acts on player 2’s payoff matrix, so and are negated while is fixed. This gives the formula for .\n\nSince the action is diagonal in these coordinates, a monomial is invariant exactly when it has even total degree in the variables negated by and even total degree in the variables negated by . Thus every invariant monomial has even total degree in\n\nand even total degree in\n\nEquivalently, because the sign-flip action does not mix monomials, a polynomial in the six mean-zero coordinates is -invariant if and only if every monomial appearing in it satisfies these two parity conditions.\n\nWe compute the Molien series for this action on the six-dimensional mean-zero payoff space. The first several coefficients are\n\nWe construct generators by applying the Reynolds operator to monomials at each degree and testing which are linearly independent of products of previously found generators (see Section 16.1). The results are summarized in Table 2.\n\n| Degree | (Molien) | From products | New generators |\n|---|---|---|---|\n| 0 | 1 | 1 | 0 |\n| 1 | 0 | 0 | 0 |\n| 2 | 9 | 0 | 9 |\n| 3 | 8 | 0 | 8 |\n| 4 | 42 | 42 | 0 |\n\nHere is the dimension of the degree- invariant subspace, not the number of generators in degree . The “new generators” column records what remains after quotienting by products of lower-degree generators.\n\nThe invariant ring is generated by the invariants listed below, all of degree or . That is, all degree-4 invariants are products of lower-degree generators. The degree- computation, together with Noether’s bound (Noether 1926), proves that no generators occur above degree . The degree- and degree- rank checks are additional consistency checks, as products of the listed generators span the Molien-predicted dimensions and .\n\n**Theorem 1 ( generators)** The invariant ring of the mean-zero payoff space under is generated by the following nine degree- invariants and eight degree- invariants. The ring closes at degree 3.\n\n*Proof*. Proved by computation. The generators are computed by applying the Reynolds operator to monomials at each degree and testing for linear independence from products of previously found generators. The details are in Section 16.1.\n\nThe 9 degree-2 generators are listed in Table 3.\n\n| Id | Expression | Interpretation |\n|---|---|---|\n| Player 1 row contrast magnitude | ||\n| Cross-player row contrast alignment | ||\n| Player 1 column contrast magnitude | ||\n| Cross-player column contrast alignment | ||\n| Player 1 interaction strength | ||\n| Interaction alignment (coordination vs anti-coordination) | ||\n| Player 2 row contrast magnitude | ||\n| Player 2 column contrast magnitude | ||\n| Player 2 interaction strength |\n\nThese are the nine quadratic monomials in satisfying the two parity conditions above. Each has a direct game-theoretic interpretation. In particular, and are separate invariants, so the quotient distinguishes player 1’s own-strategy contrast from player 2’s own-strategy contrast.\n\nThe 8 degree-3 generators (Table 4) are all triple products mixing one coordinate from each of the three types (row, column, interaction).\n\n| Id | Expression |\n|---|---|\n\nThese are the products . Each measures a coupling among one row contrast, one column contrast, and one interaction contrast. For example, measures the coupling of player 1’s column contrast with player 2’s interaction and player 1’s row contrast. A large positive value of indicates that player 1’s column contrast and row contrast are both strong and aligned with player 2’s interaction, while a large negative value indicates that they are both strong but anti-aligned with player 2’s interaction.\n\nThe cubic generators contain sign information not present in the quadratic magnitudes alone. For example, the quadratic invariants determine , but not the sign of the triple product .\n\nThe first relations (also called syzygies) occur in degree . There are three degree- relations, all of the form :\n\nIn degree , the product map has relations. These are binomial relations of the form , arising when two products of generators expand to the same underlying monomial.\n\n| Degree | Products | Rank | Syzygies |\n|---|---|---|---|\n| 4 | 45 | 42 | 3 |\n| 5 | 72 | 48 | 24 |\n\nTable 5 lists the number of relations in degrees 4 and 5. We do not claim this is a complete list of syzygies, only that these are all the relations among products of generators in these degrees.\n\nWe now express several standard game-theoretic conditions in the invariant coordinates. The point is that these conditions are invariant under relabeling, and they also become explicit polynomial equations or inequalities in the generators.\n\nThe relabeling-invariant classes below correspond to polynomial equations or inequalities in the generators (Table 6).\n\n| Class | Condition in generators | Coordinate meaning |\n|---|---|---|\n| Potential | ||\n| Anti-potential | ||\n| Zero-sum | , , | |\n| Interaction-aligned | ||\n| Interaction-opposed |\n\nA symmetric game is a two-player game in which the two players have the same strategy set and exchanging the players transposes the payoff matrices. In a chosen labeling of the shared strategy set, this means\n\nequivalently\n\nThese equations depend on the chosen identification between player 1’s row labels and player 2’s column labels. Since the quotient in this section allows independent relabelings of the two players’ strategies, the invariant version of the condition is that the game has some representative in its relabeling orbit satisfying . 3\n\nOn the other hand, the potential, anti-potential, zero-sum, and interaction-alignment conditions above are all expressible using degree- invariants alone.\n\nThe degree- generators detect more than interaction alignment. They also detect whether a player has a strictly dominant pure strategy. In a -game, dominance is controlled by the comparison between a player’s own-strategy contrast and their interaction contrast. For player 1 this comparison is versus ; for player 2 it is versus .\n\nWe say that a strict ordinal -game is solvable by iterated strict dominance if repeated elimination of strictly dominated strategies terminates at a unique strategy profile.\n\nPlayer 1 has a strictly dominant strategy if and only if one row of strictly dominates the other against both columns. In mean-zero coordinates, the payoff differences between row 1 and row 2 are\n\nwhen player 2 plays column 1, and\n\nwhen player 2 plays column 2. Row 1 strictly dominates row 2 if both differences are positive, i.e. . Row 2 strictly dominates row 1 if both are negative, i.e. . Therefore player 1 has a strictly dominant row if and only if\n\nSimilarly, player 2 has a strictly dominant column if and only if\n\nIn a strict ordinal -game, if either player has a strictly dominant strategy, iterated strict dominance terminates at a single profile: after the dominated strategy is removed, the remaining player has a strict preference between the two remaining outcomes. If neither player has a strictly dominant strategy, then no strategy is eliminated at the first step, so the process cannot begin.\n\n**Proposition 2 (Solvability)** A strict ordinal -game is solvable by iterated strict dominance if and only if\n\nEquivalently, at least one player’s own-strategy contrast exceeds their interaction contrast in magnitude.\n\nThe first inequality is exactly the condition that one row of strictly dominates the other. The second is exactly the condition that one column of strictly dominates the other. If either holds, then in a strict ordinal game the first elimination leaves the other player with a strict choice between two remaining outcomes, so elimination terminates at a unique profile. If both inequalities fail, neither player has a strictly dominated strategy at the first step, so iterated strict dominance cannot begin.\n\nEach condition is a polynomial inequality in the degree- generators:\n\nor\n\nThus solvability by iterated strict dominance is a semialgebraic condition in the quotient.\n\nWe now turn from pure-strategy dominance to fully mixed equilibria. In a fully mixed equilibrium, each player must be indifferent between their two pure strategies. For -games, these indifference conditions are two linear equations: one determines player 2’s mixing probability, and the other determines player 1’s mixing probability.\n\nFor player 1, the expected payoff difference between row 1 and row 2 is\n\nwhere is the probability that player 2 plays column 1. Setting this equal to zero gives\n\nThus player 1’s indifference equation is nondegenerate exactly when .\n\nSimilarly, if is the probability that player 1 plays row 1, player 2’s expected payoff difference between column 1 and column 2 is\n\nso player 2’s indifference equation is\n\nThis equation is nondegenerate exactly when .\n\nTherefore the determinant product of the two mixed-indifference equations is, up to the irrelevant scalar factor ,\n\nThus detects whether the mixed-indifference equations are nondegenerate. If , the equations have a unique mixed-equilibrium candidate:\n\nIt is interior (all players have positive probabilities for all strategies) whenever\n\nand\n\nIn generator coordinates, this is\n\nTherefore, the degree- generators detect whether a fully mixed equilibrium exists, and if so, whether it is interior. The degree- generator detects whether the mixed-indifference equations are nondegenerate, i.e., whether a unique mixed-equilibrium candidate exists.\n\nThe values in Table 8 depend on the particular cardinal representatives chosen for each named game. We use the following representatives:\n\n| Game | ||\n|---|---|---|\n| Prisoner’s Dilemma | ||\n| Stag Hunt | ||\n| Chicken | ||\n| Pure Coordination | ||\n| Matching Pennies |\n\nThe raw invariant values depend on the chosen cardinal representatives, so the point of the examples is not the numerical values themselves. The point is that simple combinations of the generators recover familiar strategic features. For example, records interaction alignment, while and detect strict dominance for players 1 and 2.\n\n| Game | Diagnosis | |||\n|---|---|---|---|---|\n| PD | 1 | 8 | 8 | both players dominant; interaction-aligned |\n| Stag Hunt | 16 | no dominance; interaction-aligned | ||\n| Chicken | 9 | no dominance; interaction-aligned in | ||\n| Pure Coord | 4 | pure interaction; no dominance | ||\n| Match Penn | interaction-opposed; no dominance |\n\nThe full degree- and degree- generator values for these representatives are recorded in Appendix A.\n\nThe table is diagnostic for our given representatives.The Prisoner’s Dilemma is singled out by the dominance inequalities and , so both players have strictly dominant strategies. Stag Hunt, Chicken, Pure Coordination, and Matching Pennies all fail these inequalities.\n\nThe sign of can be interpreted as the interaction alignment. The intuition is that two players are interaction-aligned if, when they jointly coordinate, the resulting payoff perturbations agree. Matching Pennies is interaction-opposed, while the other representatives are interaction-aligned. The quantity is not a complete test for coordination versus anti-coordination: Chicken is a counterexample, anti-coordination-like in its best-response structure but not interaction-opposed in this invariant sense4.\n\nRobinson and Goforth (2005) classified -games by the relative order of each player’s four payoffs, identifying games that differ only by strategy relabeling. For games with no payoff ties, their equivalence relation is the same as ours, which is that two games are equivalent if one can be obtained from the other by independently swapping rows and columns, i.e. by the action of . Robinson and Goforth’s classification gives no-tie types.\n\nWe now show how to recover those types from the invariant values. The output will be a canonical -sign vector. This makes the Robinson-Goforth type a concrete object:\n\nWe first need to define a representative for each Robinson-Goforth type. The idea is to take the lexicographically minimal sign vector obtained by applying the action to the original game. It is not strictly necessary to use the lexicographic minimum (any consistent choice will work), but it is a convenient choice that gives a unique representative for each row-column orbit.\n\nLabel the four outcomes by\n\nFor a game with no payoff ties, define the player 1 comparison vector\n\nDefine analogously from . The labeled ordinal form of the game is\n\nNot every vector in can occur. The six signs for each player must come from a transitive order of four payoffs. For example, one cannot have .\n\nRow and column swaps act on the outcome labels by\n\nThus\n\nacts on by relabeling the outcome indices in every comparison. We define the Robinson-Goforth representative of a no-tie game to be the canonical sign vector\n\nwhere lexicographic order uses . This lexicographic choice is only a naming convention: it selects one representative from each row-column orbit.\n\nFor example, using the Prisoner’s Dilemma representative\n\nwe get\n\nFor the Stag Hunt representative\n\nwe get\n\nFor the Chicken representative\n\nwe get\n\nFor a no-tie Matching Pennies representative, take\n\nThen\n\nThe standard Matching Pennies matrix has payoff ties, so it produces zero entries in this sign vector and is not one of the no-tie Robinson-Goforth types. A no-tie representative such as the one above should be used when referring to its Robinson-Goforth type.\n\nLet\n\nbe a valid vector of generator values, so that\n\nfor some -game. We define a computable map\n\nfrom invariant values to canonical Robinson-Goforth representatives.\n\nFirst extract the six contrast magnitudes from the degree- generators:\n\nThe are always non-negative because they are squares of real numbers.\n\nNow form the finite set of contrast vectors\n\nwith these magnitudes and with generator values . That is, each coordinate has the prescribed absolute value,\n\nwith the convention that if a magnitude is zero then the corresponding coordinate is zero. We then keep only those sign choices satisfying\n\nfor every\n\nThere are at most sign choices to check, and fewer when some magnitudes vanish.\n\nFor each surviving , compute the comparison-sign vector\n\nwhere\n\nand\n\nThe Robinson-Goforth representative is the canonical row-column representative\n\nThis is the desired map\n\nThe definition is independent of the chosen surviving sign pattern . Indeed, if two contrast vectors have the same full generator values, then they lie in the same orbit, since the invariant ring separates row-column relabeling orbits. Canonicalizing the sign vector removes this ambiguous relabeling.\n\nIf a payoff tie occurs, one or more entries of is , so the sign vector lies in\n\nrather than\n\nSuch a game is not one of the no-tie Robinson-Goforth types, but the same construction still returns its row-column relabeling class as a weak sign vector.\n\nThe above construction uses finite enumeration of sign patterns, but this is not the only possible way to compute the Robinson-Goforth representative from invariant values. One could instead derive case-by-case formulas for the comparison signs in terms of the generator values. Those formulas would be less transparent than the finite construction above. The enumeration is not part of the definition of the invariant quotient, being just a practical way to evaluate the map from invariant coordinates to the canonical Robinson-Goforth sign vector in the case. For actual payoff tables, the sign vector can be computed directly from payoff comparisons and then canonicalized under row and column relabeling. Alternatively, we can simply use the invariant coordinates to classify games directly, without reference to the Robinson-Goforth types at all.\n\nAs we saw, the invariant ring recovers and refines the Robinson-Goforth classification system. Robinson-Goforth keeps only the canonical -sign vector, but the invariant coordinates retain additional game information.\n\nFor example, the symmetric Prisoner’s Dilemma representatives\n\nand\n\nhave the same Robinson-Goforth representative, but different invariant values. In particular, the first has\n\nwhile the second has\n\nRobinson-Goforth records that these games have the same ordinal type. The invariant coordinates also record how far apart they are cardinally inside that type.\n\nFor interpretation, it is useful to package several degree- combinations into strategic diagnostics. Define three sector-alignment polynomials\n\nand six within-player comparison polynomials\n\nThese values are degree- diagnostics inside the quotient. The first three compare the two players’ payoff landscapes sector-by-sector (row contrast, column contrast, and interaction contrast), while the remaining six compare the relative sizes of row, column, and interaction structure within each player’s payoff function.\n\nIn particular, is positive exactly when player 1 has a strictly dominant row, while is positive exactly when player 2 has a strictly dominant column. The interaction diagnostic records whether the two players’ interaction contrasts agree in sign. We can interpret this as a measure of interaction alignment (although it is not as a complete test for coordination in the best-response sense).\n\nWe now extend the construction from the case to arbitrary -games. An -game has players, each with strategies, so its payoff space is . The main symmetry group is still the strategy-relabeling group, . An element relabels player ’s strategies by , and applies the corresponding permutation to the -th strategy coordinate in every player’s payoff array. Players remain distinguishable throughout this section (see Appendix C for discussion of the enlarged group).\n\nThis section describes the invariant ring using the same logic as in the case. First, we remove player-specific payoff means. Second, we decompose the mean-zero payoff space into contrast blocks indexed by non-empty subsets of strategy coordinates. Third, we construct relabeling-invariant polynomials from these blocks by Reynolds averaging.\n\nFor each player , let be player ’s payoff function. We write a (pure) strategy profile as . The group acts by\n\nThe same strategy relabeling is applied to every player’s payoff array because all payoff arrays are indexed by the same pure strategy profiles. For , this is the row and column relabeling we saw in the case.\n\nAs in the case, we first remove payoff means. For each player , define the player-specific payoff mean\n\nSubtracting this mean gives the mean-zero payoff array . After removing these player-specific means, the mean-zero payoff space has dimension .\n\nWe now decompose each mean-zero payoff array into contrast blocks. A contrast type is indexed by a non-empty subset . For each type and each player , let be the corresponding contrast block, with\n\nFor a particular game, let denote player ’s component of type . This component measures the part of player ’s payoff array that varies jointly with the strategy coordinates in , after averaging over the coordinates not in and subtracting lower-order effects.\n\nFor a -game, the non-empty subsets of are . For player , these are exactly the row contrast, column contrast, and interaction contrast: , , . For player : , , .\n\n**Proposition 3 (Mean-Zero Contrast Decomposition)** For every -game, the mean-zero payoff space decomposes as\n\nwhere . Thus\n\n*Proof*. Fix a player . The payoff array can be identified with an element of , with one tensor factor for each strategy coordinate. In each factor, decompose\n\nwhere is the one-dimensional constant subspace and is the -dimensional contrast subspace consisting of vectors whose coordinates sum to zero.\n\nExpanding\n\ngives one summand for each subset .5 is used in the factors indexed by and is used in the remaining factors. The summand with is the constant payoff component. That is, the player-specific payoff mean. Removing this component leaves exactly the summands indexed by non-empty subsets .\n\nFor a fixed non-empty , the corresponding block has dimension\n\nSumming over all non-empty and then over all players gives\n\nFor , each block is one-dimensional, so the contrast types are simply the non-empty subsets. For , each strategy coordinate contributes two independent contrast directions, so . In general, the dimension count is\n\nFor example, in a -game, the mean-zero subspace has dimension .\n\nThe seven contrast types are summarized in Table 9:\n\n| Type | Count of types | Dim of each block | Total per payoff array | |\n|---|---|---|---|---|\n| Main effects () | 1 | 3 | ||\n| Two-way interactions () | 2 | 3 | ||\n| Three-way interaction () | 3 | 1 | ||\nTotals |\n7 types |\n26 |\n\nThe group preserves the contrast-block decomposition. If , then the factor acts on a block whenever . If , then relabeling player ’s strategies does not affect the block . Thus relabeling can permute coordinates inside a block, but can’t turn a type- block into a type- block.\n\nFor , every block is one-dimensional. Relabeling strategy coordinate changes the sign of the contrast coordinates whose type contains . This is the sign-flip action from the section. For , the relabeling action is more complicated, as it can permute the multiple contrast directions within a block. The key point is that the relabeling action preserves the block structure, so we can analyze invariants block-by-block.\n\nWe now apply the invariant-theoretic quotient construction to arbitrary -games.\n\n**Theorem 2 (Complete Classification)** Let act on the mean-zero payoff space by relabeling each player’s strategy set. Let be any finite generating set of the invariant ring\n\nThen the map\n\nis constant on strategy-relabeling orbits and separates those orbits. Therefore two mean-zero games have the same invariant coordinates if and only if they differ by a relabeling of strategies.\n\nBy the above, for every fixed , the invariant ring gives a complete cardinal classification of -games modulo strategy relabeling and player-specific payoff shifts. The statement above takes any finite generating set as input. Theorem 6 builds a generating set explicitly via the contrast-block Reynolds construction.\n\n*Proof*. Because is finite, the invariant ring is finitely generated. If two games lie in the same orbit, all invariant polynomials take the same values on them. Conversely, finite-group invariant polynomials separate orbits. Since generate the invariant ring, equality of the generator values implies equality of every invariant polynomial, hence the two games lie in the same orbit.\n\nThe generating set need not be the smallest set that separates orbits. A separating subset is sufficient. We say a subset is separating if it is a subset that separates orbits if two points lie in the same orbit whenever every takes the same value on them. Separating subsets can be much smaller than generating sets, and the following theorem of Derksen and Kemper gives a uniform bound.\n\n**Theorem 3 (Derksen-Kemper separating bound (Derksen and Kemper 2015, Thm. 2.3.15))** For any finite group acting linearly on a finite-dimensional real vector space , the invariant ring admits a separating subset of size at most .\n\nFor the present setting, . The Derksen-Kemper bound gives an explicit upper bound on the number of polynomial invariants needed to distinguish all -orbits of -games. For this is , but the actual generating set has 17 generators, which is also separating but is not the minimum. For the bound gives invariants, considerably smaller than the full generating set ( degree- plus at least degree- generators (see Proposition 5 for the binary analog of this count).6\n\nThe degree-2 invariants in the setting generalize the nine quadratic generators from the section, which were , then , and .\n\nIn the general case, each scalar contrast is replaced by a contrast block , where is a non-empty subset of strategy indices and is the player whose payoff array is being measured. For each contrast type and each pair of players , define\n\nHere the inner product is the natural Euclidean inner product on the contrast block\n\nEquivalently, after choosing the standard contrast coordinates in the block, it is the coordinatewise sum\n\nThis is invariant under strategy relabeling because relabeling acts in the same way on and .\n\nFor each , the degree- invariants form an symmetric matrix . The diagonal entries measure the magnitude of player ’s payoff variation of type . The off-diagonal entries measure “alignment” between players and in that same contrast type.\n\nFor each of the contrast types , and each unordered pair of players with allowed, there is one degree- invariant. The number of such player pairs is .\n\n**Theorem 4 (Degree-2 Gram image)** Fix a non-empty contrast type and write\n\nThe family matrix\n\nis the Gram matrix of vectors in an -dimensional contrast block. Therefore M_S is a positive semidefinite matrix of rank at most .\n\nConversely, every positive semidefinite matrix of rank at most occurs as for some choice of contrast components .\n\n*Proof*. Fix a non-empty contrast type , and let\n\nbe the corresponding contrast block. For each player , identify the copy with , and write\n\nThen\n\nThus is exactly the Gram matrix of the vectors\n\nIt follows immediately that is positive semidefinite. Indeed, for any ,\n\nTherefore .\n\nAlso, if is the matrix whose -th column is , then\n\nHence\n\nConversely, let be any positive semidefinite matrix with\n\nBy the spectral theorem, there are orthonormal vectors and positive eigenvalues such that\n\nDefine vectors by\n\nwhere the remaining coordinates are zero. Then for every ,\n\nThus is the Gram matrix of vectors in an -dimensional contrast block.\n\nFinally, because the mean-zero payoff space decomposes as a direct sum of the contrast blocks\n\nthe components may be chosen independently inside their type- blocks. Therefore the vectors constructed above can be realized as the contrast components\n\nof some mean-zero game, for instance by setting all other contrast components to zero. Hence every positive semidefinite matrix of rank at most occurs as .\n\n**Theorem 5 (Degree-2 Family Count)** The degree- mean-zero invariant layer has one family matrix for each non-empty contrast type . Therefore its dimension is\n\nfor all . In particular, the degree- count depends on but not on .\n\n*Proof*. Let\n\nbe the standard -dimensional contrast representation of . For each non-empty , the type- contrast block is naturally isomorphic to\n\nwith the remaining tensor factors constant. Thus\n\nfor each player .\n\nDegree- invariants are -invariant bilinear pairings among the contrast blocks. If , then some strategy coordinate lies in exactly one of . In that coordinate, one block carries the contrast representation , while the other carries the trivial representation. Since has no -invariant vectors, there is no nonzero invariant pairing between and . Therefore degree- invariants only pair blocks of the same contrast type.\n\nNow fix . The representation has, up to scale, a unique invariant inner product, namely the tensor-product Euclidean inner product. Hence for every pair of players , the unique degree- invariant pairing between and is\n\nBecause , a fixed contrast type contributes one invariant for each unordered player pair . There are\n\nsuch pairs.\n\nFinally, there are non-empty contrast types . Therefore\n\nWe can verify this by Molien computation for some low-degree\n\n| 2 | 3 | 3 | 9 |\n| 3 | 7 | 6 | 42 |\n| 4 | 15 | 10 | 150 |\n| 5 | 31 | 15 | 465 |\n\nThe formula factorizes as (number of contrast types) (number of player pairs); Table 10 tabulates the count through . The contrast types depend on but not on , while the player pairs depend on alone.\n\nFor a -game, the degree- generators form families. Each family contains generators, so there are degree- generators in total. We organize these into family matrices, with one row per contrast type (Table 11):\n\n| Family (type ) | Interpretation | Generators |\n|---|---|---|\n| Payoff variation with strategy coordinate 1 | for | |\n| Payoff variation with strategy coordinate 2 | for | |\n| Payoff variation with strategy coordinate 3 | for | |\n| Joint variation with coordinates 1 and 2 | for | |\n| Joint variation with coordinates 1 and 3 | for | |\n| Joint variation with coordinates 2 and 3 | for | |\n| Joint variation with all three coordinates | for |\n\nWithin each family, the 6 generators form a symmetric matrix indexed by players:\n\nIn this subsection we describe the generator construction for the invariant ring of -games inductively, starting from the case. There are two directions of generalization: we can add a player, , or we can add a strategy, . Since every -game can be reached from by a sequence of these two steps, these two principles organize the generator structure for all finite games.\n\nWe recall the generators of the invariant ring under , now presented in the family language. The contrast types are , corresponding to row contrast, column contrast, and interaction contrast: , , . Since , each contrast block is one-dimensional for each player.\n\nThe degree-2 generators organize into 3 families of 3 (Table 12):\n\n| Family | Interpretation | |||\n|---|---|---|---|---|\n| Row contrast magnitudes and alignment | ||||\n| Column contrast magnitudes and alignment | ||||\n| Interaction magnitudes and alignment |\n\nEach family is a symmetric matrix indexed by players. The diagonal entries measure magnitudes, and the off-diagonal entry measures alignment. Since , the entries are scalar products .\n\nThe degree- generators are cross-family triple products. A product is invariant under if and only if each strategy index appears in an even number of the three types . For , the only even-parity triple of types is : index appears in and , index appears in and , so each index appears twice. The degree- generators are therefore the products\n\none from each family, with all player assignments.\n\nThe basis therefore has nine degree- generators from the family matrices and eight degree- generators from the cross-family triple products, for generators total.\n\nAdding a player changes the family layer. The contrast types for an -game are the non-empty subsets of . The contrast types for an -game are the non-empty subsets of . Thus adding player creates exactly the new types with . There are such new types. The old types remain present.\n\nFor each old type , the family matrix grows from an symmetric matrix to an symmetric matrix. Each old family gains the entries for . Each new type with contributes a new family matrix . Since has not changed, the internal contrast-block structure is also unchanged. Adding a player changes which contrast families exist and how many player-pair entries each family has, but it doesn’t change the internal invariant structure of a fixed block.\n\nAt degree , this gives the count .\n\nFor binary games, the degree- cross-family products are indexed by even-parity triples of contrast types. Adding a player increases both the number of contrast types and the number of player assignments. For example, in the step , the old binary degree- type triple is\n\nIts player assignments grow from to . There are also\n\nnew type triples involving the new coordinate . Together with the old triple, this gives type triples total. Hence\n\nThis illustrates the recurrence of Proposition 5.\n\n**Proposition 4 (Add-Player Step For the Degree-2 Family Layer)** Assume the degree- mean-zero invariant layer for -games is indexed by pairs\n\nThen the degree- mean-zero invariant layer for -games is obtained as follows.\n\nFirst, each old type\n\nremains present, and its family matrix grows from size to size . Thus each old type contributes new entries,\n\nSecond, the new contrast types are exactly the subsets\n\nwith . There are such types, and each contributes a full symmetric family matrix.\n\nTherefore the add-player increment is\n\nConsequently, if\n\nthen\n\n*Proof*. The contrast types for -games are the non-empty subsets of . The contrast types for -games are the non-empty subsets of . Hence the old types are the non-empty subsets not containing , and the new types are the subsets containing . There are new types.\n\nFor an old type , the internal contrast block has dimension , which is unchanged because and are unchanged. But there is now one contrast component for each of the players. Hence the old family matrix grows from an symmetric matrix to an symmetric matrix. This adds exactly entries, namely the entries involving the new player index .\n\nThere are old types, so old types contribute\n\nnew degree- invariants.\n\nEach new type with contributes a full symmetric family matrix\n\nSo each new type contributes\n\ndegree- invariants. Since there are new types, the new-type contribution is\n\nTherefore\n\nAdding this to the inductive hypothesis gives\n\n**Proposition 5 (Add-Player Step for Binary Degree- Triples)** For binary games, identify contrast types with nonzero vectors in , using symmetric difference as addition. Let be the number of unordered triples of distinct non-empty contrast types satisfying\n\nThen\n\nConsequently,\n\nSince each type triple admits player assignments, the binary degree- count satisfies\n\n*Proof*. For , each contrast block is one-dimensional. The strategy swap in coordinate changes the sign of exactly when . Therefore a cubic monomial\n\nis invariant if and only if each coordinate appears in an even number of . Equivalently,\n\nNow pass from players to players. The old triples are exactly the triples not containing in any of their three contrast types. These are the old triples.\n\nA genuinely new invariant triple must involve the new coordinate . Because the parity condition requires to occur an even number of times, the coordinate must occur in exactly two of the three contrast types. Thus every new triple has the form\n\nwhere and inside . Once and are chosen, we are forced to choose by .\n\nFor each fixed non-empty , there are choices of . The two new types and are distinct because . However, choosing or choosing gives the same unordered pair of new types. Therefore each fixed contributes unordered new triples.\n\nThere are choices of non-empty , so the number of genuinely new type triples is\n\nHence\n\nWith base case , this recurrence solves to\n\nFinally, for each unordered type triple , the player indices attached to the three types may be chosen independently in . Thus each type triple contributes degree- invariants, giving\n\nAdding a strategy changes the internal representation carried by each contrast type, but not the set of contrast types. Both -games and -games have one contrast family for each non-empty subset\n\nFor type , the contrast block grows from dimension\n\nto dimension\n\nThe player indexing does not change. Thus each degree- family matrix remains an symmetric matrix\n\nTherefore the degree- count is unchanged:\n\nAdding a strategy leaves the degree- family indexing fixed, while changing the internal form of the invariants inside each contrast block.\n\nFor , the contrast types remain\n\nWrite\n\nwhere , ,\n\nand\n\nThus and are main-effect contrast vectors, while is a row- and column-sum-zero interaction matrix.\n\nThe old binary cross-family cubic\n\nstabilizes to the contraction\n\nThere are such invariants, one for each player assignment .\n\nThe degree- invariant layer for decomposes by type pattern as follows (Table 13).\n\n| Type pattern | Invariant form | Count |\n|---|---|---|\n| , | ||\n| , | ||\n| , | ||\n| , | ||\n| , |\n\nTherefore\n\nThe invariants of type and are the new main-effect cubics. The invariants of type are the stabilized binary cross-family cubics. The remaining degree- invariants are not all pure interaction-family cubics. They consist of invariants of type , invariants of type , and pure interaction invariants of type .\n\n*Proof*. For , the standard contrast representation of is two-dimensional. It has, up to scale, one invariant inner product\n\nand one invariant symmetric cubic form\n\nThe main-effect blocks and each carry a copy of . Hence within one main-effect family, the degree- invariants are precisely the polarizations\n\nSince the player labels range over two players and the expression is symmetric in them, each main-effect family contributes\n\ncubic invariants. The two main-effect families therefore contribute .\n\nNext consider one , one , and one . The interaction block carries , with the first factor acted on by row relabeling and the second by column relabeling. The unique contraction is\n\nThe three player labels are independent, so this contributes\n\ninvariants.\n\nNow consider one and two ’s. The row coordinate uses the cubic invariant , while the column coordinate uses the inner product. This gives\n\nHere , while the pair is unordered because the two ’s enter symmetrically. Hence this contributes\n\ninvariants. The same argument for one and two ’s contributes another invariants.\n\nFinally, three interaction blocks use the cubic invariant in both the row and column coordinates, giving\n\nAgain are unordered player labels from a two-element set, so this contributes\n\npure interaction-family cubics.\n\nThe six type patterns have distinct multidegrees in the variables , so the corresponding invariant spaces are linearly independent. Their total dimension is\n\nThis equals the Molien coefficient for the mean-zero representation. Therefore the listed invariants span the full degree- invariant layer.\n\nThe two inductive steps above give the general construction for all -games. Adding a player changes the family layer: new contrast types appear, and existing invariant patterns acquire additional player assignments. Adding a strategy changes the internal layer: the contrast types and player labels remain fixed, but the contrast representations inside each block grow.\n\nThe point of the induction is not that the -invariant ring is literally an extension of the - or -invariant ring. Rather, the point is that every -invariant is obtained by applying the same Reynolds construction to the contrast-block decomposition, and the two inductive steps account for all possible changes in that decomposition. The classification claim of Theorem 2 says that any finite generating set of the invariant ring separates orbits; the theorem below shows that the contrast-block Reynolds construction produces one.\n\n**Theorem 6 (Inductive classification theorem)** For every and , the contrast-block Reynolds construction produces a finite set of polynomial invariants that separates strategy-relabeling orbits of mean-zero -games. Equivalently, it gives a complete invariant-theoretic classification of -games modulo strategy relabeling and player-specific payoff shifts.\n\nMore explicitly, let\n\nbe the mean-zero contrast decomposition, with\n\nApply the Reynolds operator for\n\nto monomials in the contrast-block coordinates, degree by degree. At each degree, retain a basis modulo products of invariants already retained in lower degree. Continuing up to any valid finite-generation bound, for example Noether’s bound, gives a finite homogeneous generating set for\n\nThe values of these generators separate -orbits in . Thus two mean-zero -games have the same invariant coordinates if and only if they differ by a relabeling of strategies.\n\n*Proof*. For each player , the mean-zero payoff array lies in\n\nUsing the decomposition\n\nwe obtain\n\nwhere the coordinates outside carry the trivial representation. The summand is the player-specific payoff mean. Removing these means gives\n\nThe group\n\nacts independently on the strategy coordinates. In coordinate , the block carries if , and the trivial representation if . Therefore the group action preserves the contrast type and the player label . Hence every monomial in the contrast coordinates has a well-defined block pattern\n\ncounted with multiplicity, and the Reynolds operator preserves this block pattern.\n\nNow compare with the two smaller directions.\n\nFirst, adding a player changes the set of contrast types from the non-empty subsets of to the non-empty subsets of . The old contrast types are those not containing . The new contrast types are exactly those containing . The internal representation attached to an old type does not change, since is fixed. What changes is the family bookkeeping: new types appear, and all type patterns may now be assigned to the larger player-label set . Thus the add-player step accounts for every new block pattern involving either a new contrast type or the new player label.\n\nSecond, adding a strategy changes to . The set of contrast types does not change, and neither does the set of player labels. What changes is the internal representation\n\nThus the add-strategy step accounts for every new invariant tensor that appears inside an existing type pattern after the contrast blocks enlarge.\n\nTherefore the two inductive operations account for all possible changes in the contrast-block representation: adding a player changes which block patterns exist, while adding a strategy changes the invariant tensors available inside those block patterns.\n\nIt remains to show that this construction classifies games. Since is finite, the invariant ring\n\nis finitely generated. Moreover, the Reynolds operator\n\nis a projection from onto . Hence, in each degree , Reynolds averages of degree- monomials span the full degree- invariant layer.\n\nBy proceeding degree by degree and discarding invariants generated by products of lower-degree invariants, we obtain a finite homogeneous generating set of the invariant ring. The generators are all obtained from the contrast-block Reynolds construction described above.\n\nFinally, for a finite group action, invariant polynomials separate orbits. Therefore two mean-zero games have the same values on all generators if and only if they lie in the same -orbit. Equivalently, the constructed invariant coordinates classify mean-zero -games up to strategy relabeling. Restoring the removed player-specific payoff means gives the corresponding classification of full games, with the means treated as degree- invariants.\n\nAs a computational check on the inductive construction, Table 14 compares the mean-zero Molien coefficients for several -games under the strategy-relabeling group .\n\n| Group | MZ dim | |||||\n|---|---|---|---|---|---|---|\n| 4 | 6 | 9 | 8 | 42 | ||\n| 36 | 16 | 9 | 32 | 132 | ||\n| 8 | 21 | 42 | 189 | 1428 | ||\n| 216 | 78 | 42 | 556 | 9057 |\n\nThe degree- column agrees with the family-matrix formula . So for both and , and for both and , and the degree- count depends on the number of players but not on the number of strategies.\n\nThe degree- column shows the two inductive effects separately. Adding a player, , raises the degree- count from to , creating more contrast families, more even-parity triples, and more player assignments. Adding a strategy, , raises the degree- count from to . The family structure is unchanged, but new intra-family cubic invariants appear inside the enlarged contrast blocks. The row combines both effects. There are more contrast families from the additional player and richer internal invariant rings from the additional strategy, giving and .\n\nThe interaction-alignment diagnostic from the section generalizes directly. For any -game, the -way interaction family plays the role of , and its off-diagonal entries measure pairwise alignment between players’ highest-order interaction components. A detailed treatment of the computation is given in Appendix B.\n\nThe ordinal classification of an -game is the ranking of each player’s payoff entries, considered up to strategy relabeling. In mean-zero coordinates, the ordinal type is determined by the signs of the pairwise payoff differences per player. Each difference is a linear combination of the contrast coordinates . The group acts linearly on these coordinates while preserving the contrast-block decomposition, and the generalized ordinal type is the orbit of the full pairwise-comparison sign pattern.\n\nBecause the invariant ring separates relabeling orbits of cardinal games, the full invariant values determine the cardinal relabeling class. The ordinal classification is then obtained by applying the pairwise-comparison sign map and canonicalizing the resulting sign pattern under . It remains to show how this works in practice, and how the invariant structure organizes the ordinal classification.\n\nWhen , every contrast block is one-dimensional. Thus each contrast component is a scalar, and the relabeling action is a sign-flip action indexed by the strategy coordinates contained in . A monomial is invariant exactly when each strategy coordinate appears an even number of times.\n\nFor binary games, the ordinal type modulo can be recovered by reconstructing the scalar contrast coordinates up to the sign-flip action, computing all pairwise payoff-comparison signs, and then canonicalizing the resulting sign vector under .\n\nIn low degree, the relevant sign data include:\n\nthe signs of the off-diagonal family-matrix entries for , recording interaction alignment between players within the same contrast type;\n\nthe within-player magnitude comparisons\n\nfor contrast types ;\n\nThe full values of a separating set of even-parity invariants reconstruct the scalar contrast coordinates up to relabeling. The low-degree invariants above are the first and most interpretable pieces of this reconstruction; higher even-parity products may be needed to separate all sign patterns.\n\nThe reason this works is that, for , the contrast coordinates are scalars. Every pairwise payoff difference is a linear combination of these scalar contrasts. Once the invariant values determine the scalar contrasts up to the sign-flip action, the ordinal sign vector is obtained by evaluating these linear differences and then choosing the canonical relabeling representative.\n\n*Proof*. For , each strategy coordinate has the decomposition\n\nwhere is one-dimensional. Hence every non-empty contrast block is one-dimensional. We write its scalar coordinate as\n\nThe strategy-relabeling group is\n\nLet . The relabeling acts on by\n\nThus changes sign exactly when the relabeling flips an odd number of strategy coordinates contained in .\n\nNow consider a monomial\n\nUnder , this monomial is multiplied by\n\nTherefore is invariant under all if and only if each coordinate appears in an even number of the sets . Equivalently,\n\nThis proves the even-parity rule for binary invariant monomials.\n\nNext we prove sign recovery. Suppose two binary contrast-coordinate vectors and have the same values on all invariant polynomials. In particular, they have the same quadratic invariants\n\nHence whenever , and for each nonzero coordinate there is a sign such that\n\nWe claim that the signs come from a relabeling in . To see this, consider the vector space over generated by the nonzero coordinates . Define the parity map\n\nby sending the basis vector corresponding to to the indicator vector of . A product of coordinates is invariant exactly when its exponent vector lies in .\n\nSince and have the same values on all invariant monomials, the sign character is trivial on every invariant monomial. Equivalently,\n\nfor every . Therefore factors through the quotient\n\nEquivalently, there exists a vector such that\n\nfor every nonzero coordinate . Therefore\n\nSo is obtained from by a strategy relabeling. Thus the binary invariant values reconstruct the scalar contrast coordinates up to the sign-flip action.\n\nIt remains to pass from cardinal contrast coordinates to ordinal type. For each player , the payoff at a binary strategy profile is a linear combination of the contrast coordinates , plus the player-specific mean. Therefore for two profiles , the payoff difference\n\nis a linear combination of the contrast coordinates ; the player-specific mean cancels. Hence, once the contrast coordinates are known up to relabeling, all pairwise payoff-comparison signs\n\nare determined up to the same relabeling action.\n\nTherefore, starting from the invariant values, one may enumerate the finitely many sign choices for the scalar contrast coordinates that are consistent with those invariant values. The argument above shows that all surviving choices lie in the same -orbit. For any surviving choice, compute the full pairwise payoff-comparison sign vector, and then choose its canonical representative under . The result is independent of the surviving choice.\n\nThus the binary invariant values determine the ordinal type modulo strategy relabeling. If payoff ties occur, the same argument recovers the weak ordinal sign vector with entries in .\n\nWhen , each main-effect contrast is a vector in , and the invariants built from a single contrast type are generated by the power sums . The ordinal type of a -vector with is the ranking of its entries. This ranking is determined by the signs of the pairwise differences , which define a hyperplane arrangement in . The ordinal types (modulo ) are the orbits of the chambers of this arrangement.\n\nThe power sums map to the invariant space . This map sends the hyperplane arrangement to a discriminant locus , where\n\nis a polynomial in the power sums (by Newton’s identities). The ordinal types correspond to the connected components of the complement of in invariant space.\n\nFor , the discriminant is , which vanishes only at the origin. The complement has one component, so there is one ordinal type (up to ). The two-player information comes from the family matrices and cross-family products, as described above.\n\nFor , the discriminant is\n\nExpanding in power sums, with , gives\n\nup to a positive constant. The condition is the no-tie condition for the three entries. The invariant records the skewness of the three-entry contrast vector. is not itself an ordinal type.\n\nFor , the discriminant is a degree- polynomial in . The no-tie region is defined by , while the finer chamber information is recovered by pulling back the pairwise comparison signs through the quotient map.\n\nThe ordinal classification for is therefore semialgebraic in the invariant coordinates. It is determined by polynomial inequalities, not merely by the signs of individual generators. The discriminant polynomial\n\ndefines the tie boundary for a single -entry contrast vector. In the full game, the same principle applies to every payoff-comparison hyperplane. The image of the tie boundary in the invariant quotient becomes a polynomial or semialgebraic boundary between ordinal regions.\n\nThe ordinal classification of an -game is recovered from the invariant ring in two interacting layers:\n\nThe family matrices and cross-family contractions encode the inter-player and inter-type structure. This includes which contrast types are large, which players are aligned, and how different contrast types couple.\n\nThe intra-family invariants encode the internal shape of each contrast block. For main-effect blocks this includes the power sums ; for higher-order interaction blocks it includes the Reynolds-averaged invariants of the corresponding block representation.\n\nThe two layers interact because each payoff-comparison sign is a linear condition in the original contrast coordinates, and its image in the invariant quotient is generally semialgebraic. Adding a player expands the family layer by introducing new contrast types. Adding a strategy expands the internal layer by increasing the dimension of each contrast block. Thus the same inductive construction used for the invariant ring also gives an algorithmic recovery procedure for ordinal classifications.\n\nThe invariant coordinates above classify games modulo strategy relabeling. We now record scaling laws for the invariant ring and several ways these coordinates interact with standard game-theoretic structures. The results in this section are not needed for the classification theorem itself; rather, they show how the invariant ring grows with and how equilibrium conditions, Hodge-theoretic decompositions, and cyclic witnesses can be studied inside the relabeling quotient.\n\nThe computations above extend to games of arbitrary size. Here we describe quantitative scaling laws for how the invariant ring grows with the number of players , the number of strategies , and the polynomial degree .\n\nFix the number of players and the polynomial degree . Computationally, the dimension\n\nstabilizes as the number of strategies increases. The reason is that a degree- monomial can involve at most distinct strategy labels in each coordinate. Once enough labels are available, adding more strategies should not create new degree- orbit types.\n\nFor two-player games under , the low-degree Molien coefficients are tabulated in Table 15:\n\n| 2 | 3 | 4 | 5 | 6 | |\n|---|---|---|---|---|---|\n| 2 | 9 | 8 | 42 | 48 | 138 |\n| 3 | 9 | 32 | 132 | ||\n| 4 | 9 | 32 |\n\nThe degree- count stabilizes immediately:\n\nThe degree- count similarly stabilizes at in the computed range:\n\nThe binary row is exceptional in degree , as it has only the eight cross-family cubic generators from the computation. When , new within-family cubic invariants appear, raising the count from to .\n\nWe have the following stabilization theorem.\n\n**Theorem 7 (Stabilization in the Number of Strategies)** Fix and . Then the degree- mean-zero invariant dimension\n\nstabilizes for all . Equivalently, for fixed ,\n\nThe threshold is sufficient, though not necessarily minimal.\n\n*Proof*. Let be the full payoff space. A degree- monomial has the form For each strategy coordinate , this monomial uses only the labels so it uses at most distinct labels in coordinate .\n\nThe degree- invariant space has a basis given by orbit sums of degree- monomials. Therefore its dimension is the number of degree- monomial orbits. If , every degree- monomial using labels in is equivalent, under , to one using only labels in , because at most labels are used in each coordinate. Conversely, if two monomials using only labels in are equivalent under , the permutations relating their used labels restrict to bijections between subsets of , and since , these bijections extend to permutations of . Hence they were already equivalent under .\n\nThus the degree- monomial orbit count in the full payoff space stabilizes for .\n\nFinally, equivariantly, where is the space of player-specific payoff means and is fixed by the group. Hence Removing the fixed mean variables only multiplies the Hilbert series by , so stabilization of the full-space coefficients through degree implies stabilization of the mean-zero coefficients in degree . Therefore\n\nThe previous section showed that is polynomial in . The next degree breaks that pattern. For -games under on the mean-zero subspace,\n\nA direct count of even-parity contrast-type triples (Proposition 5) gives the closed form\n\nThe leading term scales like , so the count is super-polynomial in . Numerical agreement with this formula for is verified in `degree3_mz_strategy_only.py`\n\n.\n\nThe combinatorial source is visible in the orbit structure. A degree-3 monomial is a triple of payoff coordinates. Its -orbit is determined by the Hamming pattern of strategy indices across the three coordinates, and the number of distinct Hamming patterns grows exponentially with the number of players.\n\nThe growth rate is sensitive to how players are distinguished. Restrict to fully symmetric games, those invariant under arbitrary player relabeling. There the degree- count becomes polynomial in for each . The super-polynomial behavior above is the price of keeping all players distinguishable.\n\nThe stabilization and growth results together imply a degree hierarchy of game-theoretic phenomena (Table 16):\n\n| Degree | What it detects | When new -strategy effects appear |\n|---|---|---|\n| 2 | Contrast magnitudes, interaction strength, cross-player alignment | |\n| 3 | Contrast-interaction coupling, skewness, cyclic directionality | |\n| 4 | Higher-order contrast distributions, interlocking cyclic structure, -strategy adversarial witnesses |\n\nEach degree adds invariants that detect higher-order polynomial structure in the payoff array. Some of these effects already occur at through cross-family products of scalar contrast coordinates. Others first appear when enough strategy labels are available. At , new degree- invariants appear that detect skewness and cyclic directionality, as in Rock-Paper-Scissors. At , new degree- invariants can detect more complicated interlocking cyclic patterns.\n\nThe analogy to probability moments is useful: just as variance, skewness, and kurtosis capture successively finer features of a distribution, the degree-, degree-, degree-, and higher invariants of a game capture successively finer strategic structure. Two games with the same degree- invariants but different degree- invariants are like two distributions with the same variance but different skewness.\n\nThe invariant coordinates do not depend on a solution concept, but solution concepts can still be studied inside the quotient. We begin with the full-support Nash indifference equations.\n\nThe cleanest formulation uses the tangent space of the mixed-strategy simplex. Let\n\nand let\n\nThe space is the tangent space to the affine simplex , while records payoff vectors modulo addition of a constant. A player is indifferent among all pure strategies exactly when their expected payoff vector is zero in .\n\nBoth and carry the standard -dimensional representation of . A strategy relabeling permutes coordinates, preserves , and acts on by the induced quotient action.\n\nLet be a bimatrix game. For player 1, the full-support indifference condition is\n\nwhere is player 2’s mixed strategy. Passing to the quotient by , this gives an affine linear condition\n\nThe associated linear map on tangent directions is\n\nSimilarly, player 2’s full-support indifference condition is\n\nand the associated tangent map is\n\nAfter choosing bases of and , define\n\nThe two-player full-support indifference determinant is\n\nEquivalently, in coordinates, is the determinant of the usual system whose first rows are payoff-difference equations and whose final row is the normalization equation. The same holds for .\n\n**Proposition 6 (Two-player full-support indifference determinant)** For a bimatrix game,\n\nis a -invariant polynomial of degree in the payoff entries. It is nonzero exactly when both full-support indifference systems have unique solutions. For , it agrees with , up to the sign convention used for the indifference rows.\n\n*Proof*. The map is linear in the entries of . Since and both have dimension , its determinant is homogeneous of degree in the entries of . Similarly, is homogeneous of degree in the entries of . Hence\n\nhas degree in the payoff entries.\n\nThe affine full-support indifference system for player 1 is\n\nIts linear part on the affine simplex is . Therefore the system has a unique solution if and only if is invertible, i.e. if and only if . The same argument applies to . Thus exactly when both full-support indifference systems have unique solutions.\n\nNow we prove relabeling invariance. Let , where relabels player 1’s strategies and relabels player 2’s strategies. In matrix form,\n\nThe induced map on player 1’s indifference operator is\n\nwhere and denote the induced actions on and . Taking determinants gives\n\nThe standard representation has determinant equal to the sign of the permutation, so and . Hence\n\nFor player 2, the relevant operator is . Under the same relabeling, its domain is affected by and its codomain by . Thus\n\nTherefore the product transforms as\n\nThus is invariant under .\n\nFor , the spaces and are one-dimensional. With\n\nthe tangent direction in player 2’s simplex is proportional to . Then is represented by the payoff difference\n\nThus , up to the chosen orientation. Similarly , up to orientation. Hence\n\nup to the sign convention used for the bases.\n\nThe condition does not by itself assert that an interior Nash equilibrium exists. It says that the two full-support indifference systems have unique candidate mixed strategies. These candidates must still have strictly positive coordinates to define an interior mixed profile.\n\nConversely, does not rule out interior equilibria. Degenerate games may have continua of interior equilibria. For example, if both payoff matrices are zero, every mixed profile is a Nash equilibrium, but the determinant above vanishes.\n\nFor , the full-support indifference equations are no longer linear in all mixed-strategy variables jointly. They are multilinear. Therefore the natural generalization of the two-player determinant is a Jacobian form on the payoff–mixed-strategy incidence space, not generally a payoff-only polynomial.\n\nLet be an -game. For each player , let be their mixed strategy. Write . For each player , define the expected payoff vector by\n\nPlayer is indifferent among all pure strategies exactly when\n\nThus the full-support indifference map is\n\nThe domain of variations in the mixed-strategy variables is . Thus the Jacobian of the indifference system is the linear map\n\nDefine\n\nafter choosing compatible bases of and .\n\n**Proposition 7 (Full-support Nash incidence Jacobian)** For an -game, the full-support indifference map has Jacobian determinant\n\nThis is a polynomial in the payoff entries and mixed-strategy coordinates. It is homogeneous of degree in the payoff entries. It is invariant under simultaneous strategy relabeling of payoff arrays, mixed-strategy coordinates, and indifference equations.\n\nAt a full-support equilibrium , the condition is the local nondegeneracy condition for the full-support indifference system.\n\n*Proof*. For each player , the expected payoff vector is linear in the payoff entries of player and multilinear in the mixed strategies of the other players. It does not depend on . Therefore the indifference map is polynomial in , linear in the payoff entries of each player, and multilinear in the mixed-strategy variables.\n\nThe Jacobian has row-blocks, one for each player. The row-block corresponding to player consists of the derivatives of with respect to the mixed strategies of the players . Every entry in this row-block is linear in player ’s payoff entries.\n\nThe total dimension of the domain and codomain is . Thus is an matrix. In every term of its determinant, one entry is chosen from each row. Since the rows belonging to player ’s block are each linear in player ’s payoff entries, every determinant term is homogeneous of degree in the payoff entries of player . Multiplying over all players, every term has total payoff degree . Therefore is homogeneous of degree in the payoff entries.\n\nThis determinant is not the zero polynomial. To see this, fix any interior mixed strategy profile . Choose payoff tensors so that, for each player , the indifference vector depends linearly and invertibly only on the mixed strategy of player , with indices read cyclically. In block form, the Jacobian can then be made into a cyclic block-permutation matrix with identity blocks . Such a matrix has nonzero determinant. Hence the polynomial has exact payoff degree .\n\nNow we prove invariance. A relabeling acts on payoff arrays, mixed-strategy coordinates, and payoff-vector quotients. Let denote the induced action on , and let denote the induced action on . The full-support indifference map is equivariant:\n\nDifferentiating with respect to gives\n\nTaking determinants yields\n\nBut and are the same direct sum of standard representations, one acting on simplex tangent directions and the other acting on payoff differences modulo constants. Therefore . Hence\n\nFinally, if is a full-support equilibrium, then . The local solution structure of the full-support indifference system near is controlled by the derivative . By the inverse function theorem, is locally a nonsingular solution of the indifference system exactly when this derivative is invertible. Equivalently, .\n\nFor , the indifference equations are linear in the opponent’s mixed strategy. Therefore the Jacobian form is independent of the mixed-strategy coordinates and reduces to the payoff-only determinant .\n\nFor , the Jacobian generally depends on the mixed-strategy coordinates. Substituting an equilibrium need not produce a polynomial in the payoff entries.\n\nFor example, in a -game, write for the probabilities that players play their first strategy. The binary full-support indifference equations may have the form\n\nWhen , the full-support solution is . The Jacobian is\n\nso . At the solution, , which is not a polynomial in the payoff parameter . Thus, for , the incidence-space Jacobian is the natural polynomial object. A payoff-only discriminant would require eliminating the mixed-strategy variables, for example by a resultant or discriminant construction. We leave that elimination-theoretic object as an open problem.\n\nThe payoff-only degree in has been verified numerically for seven combinations (Table 17).\n\n| Degree in payoffs | Verified | |\n|---|---|---|\n| 2 | ||\n| 4 | , verified numerically | |\n| 6 | verified numerically | |\n| 8 | verified numerically | |\n| 3 | verified numerically | |\n| 4 | verified numerically | |\n| 5 | verified numerically |\n\nThe invariant-ring construction is not the only way to decompose the space of finite games. Another important decomposition is the Hodge decomposition of Candogan et al. (2011), with dynamical extensions in Candogan, Ozdaglar, and Parrilo (2013), which separates a game into potential, harmonic, and nonstrategic components. That decomposition is linear: it splits the game space into subspaces with distinct strategic interpretations.\n\nFor a -game, the payoff space decomposes as\n\nIn the Candogan-Ozdaglar-Parrilo decomposition, consists of payoff components that do not depend on the acting player’s own strategy and has dimension . After quotienting by nonstrategic components, the normalized game space decomposes into potential and harmonic components, with the harmonic component having dimension . A game is a potential game if and only if its harmonic component vanishes. A game is harmonic if and only if its potential and nonstrategic components vanish. The decomposition is orthogonal with respect to the standard inner product on and is preserved by the strategy-relabeling group .\n\nThe invariant quotient developed in this paper has a different purpose. It does not decompose games by strategic behavior directly. Instead, it quotients games by strategy relabeling and constructs polynomial coordinates on the resulting orbit space. The two constructions are complementary. The Hodge decomposition separates the directions in game space associated with potential, harmonic, and nonstrategic structure. The invariant-ring construction then provides coordinates on these structures modulo arbitrary names assigned to strategies.\n\nIn this sense, the invariant coordinates refine the Hodge decomposition rather than replacing it. One can first project a game to its Hodge components and then evaluate relabeling-invariant polynomials on those components. Conversely, one can study how the potential, harmonic, and nonstrategic subspaces appear inside the relabeling quotient.\n\nFor example, in the mean-zero coordinates , the potential and anti-potential conditions are visible in the interaction coordinates. The potential condition is , while the anti-potential condition is . In invariant coordinates, these become quadratic conditions: for the potential case, and for the anti-potential case. Thus a linear strategic decomposition can appear inside the invariant quotient as polynomial equations among invariant coordinates.\n\nMore generally, whenever a linear game class is preserved by strategy relabeling, the invariant coordinates restrict to polynomial coordinates on that class modulo relabeling. This allows potential, harmonic, nonstrategic, zero-sum, and other linear or affine game classes to be studied inside the same quotient framework.\n\nThe important distinction is that the Hodge decomposition describes where a game lies in the original payoff space, while the invariant-ring construction describes where its relabeling orbit lies in the quotient. The first is a linear decomposition of games; the second is a polynomial classification of games up to labels.\n\nSome strategic patterns are naturally witnessed by products of payoff comparisons around cycles. This gives a useful source of interpretable invariants. However, cycle length should not be treated as a universal lower bound on detection degree. Lower-degree invariants may detect coarser shadows of the same structure.\n\nSuppose a strategic pattern is supported on a minimal payoff-comparison cycle of length . Let be linear payoff-comparison forms associated with the edges of that cycle. The product\n\nis a degree- polynomial witness for the oriented cycle. Averaging this witness over relabelings gives an invariant polynomial.\n\n**Proposition 8 (Cyclic witness invariants)** Suppose a strategic pattern has a label-complete cyclic witness\n\nwhere each is a linear payoff-comparison form supported on one edge of a minimal -cycle. Then the Reynolds average\n\nis a strategy-relabeling invariant of degree .\n\nIf an allowed relabeling sends the oriented witness to , then the sign of does not descend to the relabeling quotient. In that case\n\nor an equivalent paired product gives a natural invariant witness of degree .\n\nThis is an existence statement for natural cycle witnesses, not a universal lower bound on the degree at which every feature of the pattern can be detected.\n\n*Proof*. Each payoff-comparison form is linear in the payoff entries. Hence\n\nhas degree . Applying the Reynolds operator gives\n\nwhich is invariant by construction and still has degree . Thus a label-complete cyclic witness gives a natural degree- invariant.\n\nIf an allowed relabeling sends to , then the sign of is not well-defined on the quotient: two relabeling-equivalent representatives give opposite values of the oriented witness. Therefore itself cannot serve as a signed quotient coordinate for that orientation.\n\nHowever, is unchanged by , and its Reynolds average is an invariant polynomial of degree . Similarly, if two oriented witnesses transform with opposite signs, then their product gives a degree- invariant.\n\nThis proves the existence of natural degree- cycle witnesses and degree- squared or paired witnesses. It does not prove that lower-degree invariants cannot detect weaker or coarser aspects of the same strategic pattern.\n\nThe binary Matching Pennies line illustrates why the cycle-witness degree should not be read as a universal lower bound. Consider the line in the mean-zero space parameterized by\n\nAlong this line, and . The degree- invariants record\n\nThus the basic adversarial interaction is already visible at degree through .\n\nAll degree- generators in the strategy-only quotient vanish on this line because they involve at least one row or column contrast. Higher-degree oriented cycle witnesses may encode more refined cyclic structure, especially under larger symmetry groups such as the wreath-product quotient, but the degree- invariant already detects the interaction opposition in the strategy-only quotient.\n\nTherefore cycle products provide useful interpretable invariants, but classification difficulty is not governed by cycle length alone. The invariant degree required to distinguish a property depends on the exact symmetry group, the quotient being used, and the level of structure one wants to detect.\n\nAll computations in this paper follow the same algorithmic pipeline, which is describe in this section. There are three main steps: computing the group action, computing the Molien series, and computing generators and syzygies. Each step is implemented in standalone Python scripts using NumPy and SymPy.\n\nGiven an -game, the group has order . Each element is a tuple of permutations, one per player. The element acts on the payoff tensor of player by permuting the strategy indices:\n\nThis action is linear on the payoff space , so each group element is represented by a permutation matrix of size .\n\nThe Molien series is computed by the eigenvalue method. For each group element , we compute the eigenvalues of the representation matrix restricted to the relevant subspace (full, mean-zero, or interaction). The contribution of to the Molien series is\n\nwhich we expand as a power series in to the desired degree, then average over the group. For the groups , the computation reduces to a sum over elements, each requiring an eigenvalue computation. The conjugacy-class reduction groups elements with the same eigenvalues, reducing the sum to a sum over conjugacy classes weighted by class sizes.\n\nGenerators are found by the Reynolds-operator method:\n\nRing closure at degree is verified by checking that the products of all generators at degrees span a space of dimension (the Molien coefficient). By Noether’s bound, the ring is generated in degrees at most .\n\nSyzygies are found by exact symbolic expansion:\n\nEach null vector is a syzygy, linear combinations of products that vanish identically as a polynomial function on .\n\nAll computations are implemented in standalone Python scripts using NumPy and SymPy. No external computer algebra systems are required. Each script has a `__main__`\n\nblock and can be run directly. The scripts and their roles are listed in Appendix D.\n\nWe have developed an invariant-theoretic framework for finite normal-form games under strategy relabeling. For -games under , after removing player-specific payoff means, the invariant ring has 17 generators, 9 in degree 2 and 8 in degree 3. By Noether’s bound and the degree-4 span computation, no higher-degree generators are needed. The degree-2 generators are the entries of three family matrices\n\nand the degree-3 generators are the cross-family triple products mixing row, column, and interaction contrasts. The full invariant values recover the 144 Robinson-Goforth no-tie ordinal types by mapping each game to its canonical 12-sign pairwise-comparison vector. The degree-2 diagnostics give useful interpretations inside this quotient, but they are not the recovery map itself. Nine degree-2 polynomials (three alignment signs and six cross-family comparisons) recover the 144 Robinson-Goforth ordinal types via their sign vectors. The invariant values therefore refine the ordinal taxonomy by providing continuous cardinal coordinates within each ordinal type.\n\nFor general -games, the invariant ring is organized by contrast type. After removing player-specific means, each payoff array decomposes into contrast blocks indexed by non-empty subsets\n\nAt degree , these blocks produce family matrices\n\none for each contrast type . Hence the degree- mean-zero invariant count is\n\nindependent of . These matrices measure the magnitude of each contrast type for each player and the alignment between players within that contrast type.\n\nHigher-degree invariants therefore record structure not visible at degree , such as cross-family couplings, skewness, cyclic directionality, and higher-order interaction patterns. Adding a player expands the family layer by introducing new contrast types. Adding a strategy expands the internal layer by increasing the dimension of each contrast block and introducing new Reynolds-averaged internal invariants. Thus the invariant ring gives a systematic coordinate system for games modulo strategy relabeling, with standard game classes, dominance conditions, ordinal regions, and equilibrium degeneracies appearing as algebraic or semialgebraic conditions in these coordinates.\n\nThe question we are attempting to answer here is simple: given a numerical representation of the payoffs of a normal-form game, what kind of game is it? The invariant-theoretic viewpoint is a coordinate system to characterize the type of game, not a new solution concept. These coordinates can then be used to compare games, detect game classes, locate ordinal regions, and study equilibrium degeneracies.\n\nThe invariants come in layers. The degree- layer is the most directly interpretable. The diagonal entries of the family matrices measure how strongly each player’s payoff depends on each contrast type. The off-diagonal entries measure alignment between players within that same contrast type. In -games this recovers familiar quantities such as dominance strength, interaction strength, and interaction alignment. In larger games, the same objects measure whether payoff variation is mostly driven by main effects, lower-order interactions, or higher-order interactions among multiple strategy coordinates.\n\nHigher-degree invariants measure structure that cannot be seen from magnitudes and pairwise alignments alone. Cubic invariants record directional coupling among contrast types and detect skewness in non-binary strategy spaces. Higher-degree invariants detect increasingly fine cyclic and adversarial patterns. This gives a degree hierarchy, where low-degree invariants describe coarse strategic geometry, while higher-degree invariants distinguish more delicate strategic features.\n\nThis framework also clarifies the relation between cardinal and ordinal classification. Ordinal taxonomies divide game space into regions determined by payoff-comparison signs. The invariant ring retains the full cardinal geometry inside and across those regions. In the case, the invariant values recover the Robinson-Goforth no-tie ordinal types, while also preserving continuous payoff information within each type. For larger games, where exhaustive ordinal enumeration becomes infeasible, invariant coordinates provide a more scalable way to organize the space.\n\nComputationally, the framework suggests a practical workflow. For small games, one can compute explicit generators and relations. For larger games, one can work degree by degree. Start with degree- family matrices give a compact first summary, while higher-degree Reynolds averages can be added as needed for finer classification. A researcher interested only in dominance or interaction alignment may need only low-degree invariants, or a researcher studying cycling, degeneracy, or equilibrium bifurcation may need higher-degree invariants. Alternatively, given a particular sample, one could greedily search for low-degree invariants that distinguish it from a reference set of games, without needing to compute the full invariant ring.\n\nThe main limitation is that the full invariant ring becomes large quickly. The case admits a complete hand-readable description, but higher cases require significant computation. However, the algerbraic characterization of the invariant ring gives a systematic way to organize these computations and interpret their results, and opens the door to new possibilities for game classification, analysis, and control drawing from invariant theory and algebraic geometry.\n\nFor the named-game representatives of Table 7, the full degree-2 invariant values are given in Table 18 and the full degree-3 values in Table 19.\n\n| Game | |||||||||\n|---|---|---|---|---|---|---|---|---|---|\n| PD | 9 | 49 | 1 | 1 | 49 | 9 | 1 | ||\n| Stag Hunt | 4 | 16 | 16 | 16 | 16 | 4 | 16 | ||\n| Chicken | 1 | 49 | 9 | 9 | 49 | 1 | 9 | ||\n| Pure Coord | 0 | 0 | 0 | 0 | 4 | 4 | 0 | 0 | 4 |\n| Match Penn | 0 | 0 | 0 | 0 | 16 | 0 | 0 | 16 |\n\n| Game | ||||||||\n|---|---|---|---|---|---|---|---|---|\n| PD | 21 | 21 | 21 | 21 | ||||\n| Stag Hunt | 16 | 16 | 64 | 64 | ||||\n| Chicken | 21 | 21 | 21 | 21 | ||||\n| Pure Coord | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |\n| Match Penn | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |\n\nForthcoming.\n\nIn the main text, we treat the players as distinguishable and use the direct product as the symmetry group. In some settings (e.g., evolutionary biology, anonymous mechanism design), the players are interchangeable: swapping who is player 1 and who is player 2 produces an equivalent game. When the players are interchangeable, we can additionally permute the players themselves. In an -player -game with payoff arrays , a permutation acts by simultaneously permuting the payoff arrays and their indices:\n\nThe payoff array that belonged to player now belongs to player , and the strategy indices are permuted accordingly. For a two-player game, the transposition acts by . The transpose appears because swapping who is the row player and who is the column player exchanges the matrix indices. Combining strategy relabeling with player permutation requires the semidirect product, which we now define.\n\nWe call a bijection a group automorphism if for all . Let and be groups, and suppose acts on by automorphisms (meaning each defines a group automorphism of ). We call the semidirect product the group with underlying set and multiplication\n\nwhere is the result of acting on . When acts trivially ( for all ), this reduces to the direct product.\n\nFor an -game, the strategy permutations form and the player permutations form . A player permutation acts on by rearranging the factors: the strategy permutation that was acting on player now acts on player . Explicitly,\n\nWe call the resulting semidirect product the wreath product of with , sometimes written . It has elements. The direct product does not suffice because the player permutation rearranges which strategy permutation acts on which player: first relabeling player 1’s strategies and then swapping players gives a different result than first swapping and then relabeling.\n\nFor -games, the wreath product is , which is the dihedral group of order 8.\n\nIn this appendix we compute the invariant ring of -games under the enlarged group (order 8), which includes the player swap in addition to the strategy relabeling of the main text. This is the appropriate symmetry group when the two players are interchangeable (anonymous games). The ring has 22 generators (compared to 17 under ), with non-trivial syzygies involving an advantage asymmetry quantity . The ring generalizes the 78-type classification of Rapoport and Guyer (1966), who additionally identified the two players, just as the ring generalizes the 144-type classification of Robinson and Goforth (2005).\n\nWe work in the same mean-zero coordinates defined in the -Games section. The player swap acts on these coordinates as , , , derived from .\n\nWe use the same normalization conventions as the main text: unnormalized coordinates (no factor of ) and orbit-sum Reynolds operator (following Sturmfels 2008). All invariant values on games with integer payoffs are integers.\n\nThe Molien series for acting on is\n\nThe following table decomposes into products and new generators at each degree.\n\n| Degree | (Molien) | From products | New generators |\n|---|---|---|---|\n| 0 | 1 | 1 | 0 |\n| 1 | 0 | 0 | 0 |\n| 2 | 5 | 0 | 5 () |\n| 3 | 4 | 0 | 4 () |\n| 4 | 23 | 15 | 8 () |\n| 5 | 24 | 19 | 5 () |\n| 6 | 71 | 71 | 0 (ring closes) |\n\nThe ring has generators in total. We verify that no new generators appear at degrees 6, 7, or 8 by checking that the products of the 22 generators span spaces of dimension 71, 84, and 186 respectively, matching the Molien coefficients. By Noether’s bound, the invariant ring of a finite group of order is generated in degrees at most . Since , no new generators can appear above degree 8, and the 22 generators are a complete generating set.\n\n| Id | Deg | Expression |\n|---|---|---|\n| 2 | ||\n| 2 | ||\n| 2 | ||\n| 2 | ||\n| 2 | ||\n| 3 | ||\n| 3 | ||\n| 3 | ||\n| 3 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 4 | ||\n| 5 | ||\n| 5 | ||\n| 5 | ||\n| 5 | ||\n| 5 |\n\nEach generator is an orbit sum of a monomial under . The pairing and in each expression reflects the player swap .\n\nThe invariant is the product of the two players’ interaction terms. Its sign separates coordination-type games () from anti-coordination-type games (). The invariant also equals the Nash equilibrium discriminant: an interior NE exists if and only if .\n\nThe invariants and measure advantage magnitudes, but each mixes two players’ coordinates. This mixing is forced by the player swap: since under the swap, no invariant can separate from . The degree-4 generator resolves this: from and together, one can recover , which measures how evenly the advantage is split between the two players.\n\nThe invariant measures cross-player advantage alignment. The quantity will appear as the central syzygy quantity.\n\nThe degree-3 generators are the lowest-degree invariants that involve all three types of coordinates (, , and ) simultaneously. They measure how the advantage structure couples to the interaction structure. For games with no advantage structure (, such as Pure Coordination and Matching Pennies), all cubic generators vanish.\n\nThe 22 generators define a map that sends each game to its invariant values. Two games lie in the same -orbit if and only if they have the same image under .\n\nThe 22 generators are not algebraically independent. We find syzygies by expanding each product of generators as a polynomial in , collecting the monomial coefficients into an integer matrix, and computing its exact null space (see Appendix D; script: `game_invariants/syzygies_exact.py`\n\n).\n\nAt degree 4, all 15 products are linearly independent. The first syzygy appears at degree 5:\n\nAt degree 6, there are two syzygies. Both factor through the advantage asymmetry\n\nwhich is the squared difference between player 1’s row-column advantage product and player 2’s. The degree-6 syzygies are\n\n| Degree | Products | Rank | Syzygies | Notes |\n|---|---|---|---|---|\n| 4 | 15 | 15 | 0 | All independent |\n| 5 | 20 | 19 | 1 | |\n| 6 | 45 | 43 | 2 | , both through |\n| 7 | 60 | 55 | 5 | |\n| 8 | 120 | 106 | 14 |\n\nWhen , the syzygies simplify to and , which together imply and . On the advantage-symmetric locus (), the four cubic generators collapse to one degree of freedom.\n\nAll five named games satisfy , meaning named games live on the advantage-symmetric locus, a measure-zero subset of the full space. When , the syzygies express the squared cubic invariants as products of with the interaction invariants and , constraining the cubic invariants once the advantage asymmetry is known.\n\n| Class | Condition on invariants | Derivation |\n|---|---|---|\n| Potential | iff | |\n| Anti-potential | iff | |\n| Symmetric | and | Potential + advantage symmetry |\n| Zero-sum | and | Anti-potential + advantage symmetry (necessary, not sufficient at degree 2) |\n| Coordination type | : interactions aligned | |\n| Anti-coordination type | : interactions opposed |\n\nThe Nash equilibrium discriminant for -games is . An interior Nash equilibrium exists if and only if .\n\nDegree-2 invariants:\n\n| Game | |||||\n|---|---|---|---|---|---|\n| PD | 18 | 98 | 2 | 1 | |\n| Stag Hunt | 8 | 32 | 32 | 16 | |\n| Chicken | 2 | 98 | 18 | 9 | |\n| Pure Coord | 0 | 0 | 0 | 8 | 4 |\n| Match Penn | 0 | 0 | 0 | 32 |\n\nDegree-3 invariants:\n\n| Game | ||||\n|---|---|---|---|---|\n| PD | 42 | 42 | ||\n| Stag Hunt | 32 | 128 | ||\n| Chicken | 42 | 42 | ||\n| Pure Coord | 0 | 0 | 0 | 0 |\n| Match Penn | 0 | 0 | 0 | 0 |\n\nDegree-4 invariants:\n\n| Game | ||||||||\n|---|---|---|---|---|---|---|---|---|\n| PD | 162 | 882 | 18 | 4802 | 98 | |||\n| Stag Hunt | 32 | 128 | 128 | 512 | 512 | |||\n| Chicken | 2 | 98 | 18 | 4802 | 882 | |||\n| Pure Coord | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |\n| Match Penn | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |\n\nDegree-5 invariants:\n\n| Game | |||||\n|---|---|---|---|---|---|\n| PD | 378 | 2058 | |||\n| Stag Hunt | 128 | 512 | 2048 | ||\n| Chicken | 42 | 2058 | |||\n| Pure Coord | 0 | 0 | 0 | 0 | 0 |\n| Match Penn | 0 | 0 | 0 | 0 | 0 |\n\n| Game | Potential | Symmetric | Coord type | Pure NE | |||\n|---|---|---|---|---|---|---|---|\n| PD | 0 | 4 | 0 | yes | yes | yes | |\n| Stag Hunt | 0 | 64 | 0 | yes | yes | yes | |\n| Chicken | 0 | 36 | 0 | yes | yes | yes | |\n| Pure Coord | 0 | 16 | 0 | yes | yes | yes | |\n| Match Penn | 64 | 0 | 0 | no | no | no | none |\n\nAll four cooperative games are potential and symmetric. Matching Pennies is anti-potential. All five satisfy , confirming that named games are advantage-symmetric.\n\nFor -games, player 1 has a dominant strategy if and only if and have the same sign. These conditions cannot be expressed as polynomial conditions on , because the invariants mix with (the player swap pairs them). Solvability is a semialgebraic condition. The degree-4 invariants and partially resolve this ambiguity.\n\nRapoport and Guyer (1966) classified -games into 78 strict ordinal types. Robinson and Goforth (2005) extended this to 144 types by distinguishing the two players’ roles.\n\nThe ordinal type is determined by the signs of the six pairwise payoff differences per player. In mean-zero coordinates, the differences for player 1 are\n\nThe 12 hyperplanes (6 per player) partition into chambers. Each chamber is a strict ordinal type.\n\nThe ordinal and invariant-ring classifications are related but not equivalent, in two ways. First, the ordinal map is piecewise-constant while is continuous. Two games in the same chamber have the same ordinal type but generically different invariant values. Second, the two maps quotient by different groups. Robinson and Goforth quotient by (strategy relabeling only), while the invariant ring additionally identifies the two players. For symmetric games (), the two quotients agree. For asymmetric games, the player swap can merge Robinson-Goforth classes. For example, the game has a orbit of size 8 containing two Robinson-Goforth classes of size 4.\n\nThe Rapoport-Guyer count of 78 types corresponds to the quotient of the ordinal chambers: generic orbits, plus additional types from orbits of size 4 on the symmetric locus.\n\nThe invariant ring is finer than the ordinal classification in the cardinal direction (it retains magnitudes) and coarser in the player-labeling direction (it identifies the two players).\n\nAll computations are reproducible from the companion code repository. Each script is standalone Python with only NumPy and SymPy as dependencies, and is in `demonstrandom-public-code/invariants/game_invariants/`\n\n. Each has a `__main__`\n\nblock runnable directly with `python scriptname.py`\n\n.\n\nThe scripts are reproduced below for reference. The cleaned-up versions trim docstrings and exploratory print formatting; the algorithm itself is unchanged from the source files.\n\nThe 17 generators of the invariant ring under strategy relabeling are computed by Reynolds-averaging degree- monomials and testing linear independence against products of previously found generators. Ring closure is verified at degrees 4, 5, and 6.\n\n`game_invariants/generators_2x2_strategy_only.py`\n\n:\n\n␃WPNCODE0␃\n\nThe 22 generators of the invariant ring under the wreath product (including player swap) are computed by the same method. The action on has three generators: row flip, column flip, and the player swap sending , , (derived from ).\n\n`game_invariants/generators_2x2_mz.py`\n\n:\n\n``` python\nimport numpy as np\nfrom itertools import combinations_with_replacement\nfrom sympy import symbols, expand\n\ndef build_d4():\n    \"\"\"All 8 elements of D_4 = (S_2 x S_2) >| S_2 as 6x6 matrices.\"\"\"\n    s1 = np.diag([-1, 1, -1, -1, 1, -1.0])\n    s2 = np.diag([1, -1, -1, 1, -1, -1.0])\n    sw = np.array([[0,0,0,0,1,0],[0,0,0,1,0,0],[0,0,0,0,0,1],\n                   [0,1,0,0,0,0],[1,0,0,0,0,0],[0,0,1,0,0,0]], dtype=float)\n    group, seen, queue = [], set(), [np.eye(6)]\n    while queue:\n        g = queue.pop()\n        key = tuple(g.flatten().round(10))\n        if key in seen:\n            continue\n        seen.add(key)\n        group.append(g)\n        queue.extend([g @ s1, g @ s2, g @ sw])\n    return group\n\ndef reynolds_symbolic(exp_vec, group):\n    rA, cA, dA, rB, cB, dB = symbols('rA cA dA rB cB dB')\n    sym_vars = [rA, cA, dA, rB, cB, dB]\n    total = 0\n    for g in group:\n        gv = [sum(int(g[i, j]) * sym_vars[j] for j in range(6)) for i in range(6)]\n        mono = 1\n        for k, e in enumerate(exp_vec):\n            mono *= gv[k] ** e\n        total += mono\n    return expand(total / len(group))\n\ndef reynolds_numerical(exp_vec, group, points):\n    vals = np.zeros(len(points))\n    for g in group:\n        for j, pt in enumerate(points):\n            gpt = g @ pt\n            v = 1.0\n            for k, e in enumerate(exp_vec):\n                v *= gpt[k] ** e\n            vals[j] += v\n    return vals / len(group)\n\ndef eval_known_generators(pt):\n    \"\"\"I0..I4 (deg 2) and J0..J3 (deg 3).\"\"\"\n    rA, cA, dA, rB, cB, dB = pt\n    I0 = dA**2 + dB** 2\n    I1 = rA**2 + cB** 2\n    I2 = cA**2 + rB** 2\n    I3 = dA * dB\n    I4 = rA * rB + cA * cB\n    J0 = rA * cA * dA + rB * cB * dB\n    J1 = rA * cA * dB + rB * cB * dA\n    J2 = cA * rB * (dA + dB)\n    J3 = rA * cB * (dA + dB)\n    return np.array([I0, I1, I2, I3, I4, J0, J1, J2, J3])\n\ndef find_new_generators(target_degree, group, n_pts=300, seed=42):\n    \"\"\"New generators at target_degree, independent of products of lower-degree ones.\"\"\"\n    rng = np.random.default_rng(seed)\n    pts = rng.standard_normal((n_pts, 6))\n    vals = np.array([eval_known_generators(pt) for pt in pts])\n    degs = [2]*5 + [3]*4\n\n    products = []\n    for i in range(9):\n        for j in range(i, 9):\n            if degs[i] + degs[j] == target_degree:\n                products.append(vals[:, i] * vals[:, j])\n        for j in range(i, 9):\n            for k in range(j, 9):\n                if degs[i] + degs[j] + degs[k] == target_degree:\n                    products.append(vals[:, i] * vals[:, j] * vals[:, k])\n\n    product_matrix = np.array(products) if products else np.zeros((0, n_pts))\n    base_rank = np.linalg.matrix_rank(product_matrix, tol=1e-8)\n\n    mono_list = []\n    for combo in combinations_with_replacement(range(6), target_degree):\n        exp = [0] * 6\n        for c in combo:\n            exp[c] += 1\n        if tuple(exp) not in mono_list:\n            mono_list.append(tuple(exp))\n\n    current, current_rank = product_matrix.copy(), base_rank\n    new_gens = []\n    for m in mono_list:\n        r_vals = reynolds_numerical(m, group, pts)\n        test = np.vstack([current, r_vals.reshape(1, -1)])\n        r = np.linalg.matrix_rank(test, tol=1e-8)\n        if r > current_rank:\n            new_gens.append((m, reynolds_symbolic(m, group)))\n            current, current_rank = test, r\n    return new_gens, base_rank, current_rank\n\nif __name__ == '__main__':\n    G = build_d4()\n    print(f\"D_4 order: {len(G)}\")\n    expected = {2: 5, 3: 4, 4: 8, 5: 5, 6: 0}\n    for deg in [2, 3, 4, 5, 6]:\n        new_gens, prod_rank, total_rank = find_new_generators(deg, G)\n        print(f\"Degree {deg}: products rank {prod_rank}, total rank {total_rank}, \"\n              f\"new generators {len(new_gens)} (expect {expected[deg]})\")\n```\n\nSyzygies are computed by exact symbolic expansion. For each target degree , we enumerate all products of generators of total degree , expand each as a polynomial in , collect monomial coefficients into an integer matrix , and compute the null space of over using SymPy. Each null vector is a syzygy.\n\n`game_invariants/syzygies_exact.py`\n\n:\n\n``` python\nfrom sympy import symbols, expand, Poly, Matrix, factor\n\nrA, cA, dA, rB, cB, dB = symbols('rA cA dA rB cB dB')\nVARS = [rA, cA, dA, rB, cB, dB]\n\nI = [rA**2 + cB** 2, rA*rB + cA*cB, cA**2 + rB** 2, dA**2 + dB** 2, dA*dB]\nJ = [cA*dA*rA + cB*dB*rB, cA*dB*rA + cB*dA*rB,\n     cB*rA*(dA + dB), cA*rB*(dA + dB)]\nI_NAMES = ['I0', 'I1', 'I2', 'I3', 'I4']\nJ_NAMES = ['J0', 'J1', 'J2', 'J3']\n\ndef find_syzygies_at_degree(target_deg):\n    gens = I + J\n    names = I_NAMES + J_NAMES\n    degs = [2]*5 + [3]*4\n    n = len(gens)\n\n    products, prod_names = [], []\n    for i in range(n):\n        for j in range(i, n):\n            if degs[i] + degs[j] == target_deg:\n                products.append(expand(gens[i] * gens[j]))\n                prod_names.append(f'{names[i]}*{names[j]}')\n        for j in range(i, n):\n            for k in range(j, n):\n                if degs[i] + degs[j] + degs[k] == target_deg:\n                    products.append(expand(gens[i] * gens[j] * gens[k]))\n                    prod_names.append(f'{names[i]}*{names[j]}*{names[k]}')\n\n    if not products:\n        return [], [], 0\n\n    polys = [Poly(p, VARS) for p in products]\n    monos = sorted({m for p in polys for m in p.as_dict()})\n    M = Matrix([[p.as_dict().get(m, 0) for p in polys] for m in monos])\n    return prod_names, M.nullspace(), M.rank()\n\ndef format_syzygy(prod_names, vec):\n    pos = [(vec[i], prod_names[i]) for i in range(len(prod_names)) if vec[i] > 0]\n    neg = [(-vec[i], prod_names[i]) for i in range(len(prod_names)) if vec[i] < 0]\n    lhs = ' + '.join(f'{c}*{n}' if c != 1 else n for c, n in pos)\n    rhs = ' + '.join(f'{c}*{n}' if c != 1 else n for c, n in neg)\n    return f'{lhs} = {rhs}'\n\nif __name__ == '__main__':\n    for deg in [4, 5, 6]:\n        names, null_vecs, rank = find_syzygies_at_degree(deg)\n        print(f'Degree {deg}: {len(names)} products, rank {rank}, {len(null_vecs)} syzygies')\n        for idx, vec in enumerate(null_vecs):\n            print(f'  S{idx+1}: {format_syzygy(names, vec)}')\n    D = expand(I[0]*I[2] - I[1]**2)\n    print(f'D = I0*I2 - I1^2 = {factor(D)}')\n```\n\nMolien series for -games under the wreath product are computed via conjugacy classes parameterized by bipartitions, using Newton’s identity to recover the degree- coefficient from fixed-point counts of .\n\n`game_invariants/scaling.py`\n\n:\n\n``` python\nfrom collections import Counter\nfrom math import factorial\n\ndef partitions(n, max_val=None):\n    if max_val is None:\n        max_val = n\n    if n == 0:\n        yield ()\n        return\n    for k in range(min(n, max_val), 0, -1):\n        for rest in partitions(n - k, k):\n            yield (k,) + rest\n\ndef bipartitions(n):\n    \"\"\"Conjugacy classes of S_n wr Z_2 are bipartitions (alpha, beta) of n.\"\"\"\n    for a in range(n + 1):\n        for alpha in partitions(a):\n            for beta in partitions(n - a):\n                yield alpha, beta\n\ndef centralizer_order(alpha, beta):\n    result = 1\n    for parts in [alpha, beta]:\n        for k, m in Counter(parts).items():\n            result *= (2 * k) ** m * factorial(m)\n    return result\n\ndef class_size(alpha, beta, n):\n    return factorial(n) * 2 ** n // centralizer_order(alpha, beta)\n\ndef build_representative(alpha, beta, n):\n    \"\"\"Induced permutation on n * 2^n coordinates for bipartition (alpha, beta).\"\"\"\n    num_profiles = 2 ** n\n    dim = n * num_profiles\n    sigma = list(range(n))\n    epsilon = [0] * n\n    pos = 0\n    for k in alpha:\n        for i in range(k - 1):\n            sigma[pos + i] = pos + i + 1\n        sigma[pos + k - 1] = pos\n        pos += k\n    for k in beta:\n        for i in range(k - 1):\n            sigma[pos + i] = pos + i + 1\n        sigma[pos + k - 1] = pos\n        epsilon[pos] = 1\n        pos += k\n\n    inv_sigma = [0] * n\n    for i in range(n):\n        inv_sigma[sigma[i]] = i\n\n    perm = [0] * dim\n    for p in range(n):\n        for prof_int in range(num_profiles):\n            prof = [(prof_int >> (n - 1 - q)) & 1 for q in range(n)]\n            src_prof = [0] * n\n            for q in range(n):\n                sq = inv_sigma[q]\n                src_prof[sq] = prof[q] ^ epsilon[sq]\n            src_int = 0\n            for q in range(n):\n                src_int = (src_int << 1) | src_prof[q]\n            perm[p * num_profiles + prof_int] = inv_sigma[p] * num_profiles + src_int\n    return perm\n\ndef build_player_permutation(alpha, beta, n):\n    sigma = list(range(n))\n    pos = 0\n    for parts in [alpha, beta]:\n        for k in parts:\n            for i in range(k - 1):\n                sigma[pos + i] = pos + i + 1\n            sigma[pos + k - 1] = pos\n            pos += k\n    return sigma\n\ndef fixed_points_of_power(perm, power):\n    count = 0\n    for i in range(len(perm)):\n        j = i\n        for _ in range(power):\n            j = perm[j]\n        if j == i:\n            count += 1\n    return count\n\ndef molien_conj(n, max_degree, mean_zero=False):\n    \"\"\"Molien series via Newton's identity over conjugacy classes.\"\"\"\n    group_order = factorial(n) * 2 ** n\n    result = [0] * (max_degree + 1)\n    for alpha, beta in bipartitions(n):\n        csize = class_size(alpha, beta, n)\n        perm = build_representative(alpha, beta, n)\n        player_perm = build_player_permutation(alpha, beta, n) if mean_zero else None\n        p = []\n        for k in range(max_degree + 1):\n            pk = fixed_points_of_power(perm, k)\n            if mean_zero:\n                pk -= fixed_points_of_power(player_perm, k)\n            p.append(pk)\n        h = [0] * (max_degree + 1)\n        h[0] = 1\n        for d in range(1, max_degree + 1):\n            h[d] = sum(p[k] * h[d - k] for k in range(1, d + 1)) // d\n        for d in range(max_degree + 1):\n            result[d] += csize * h[d]\n    return [r // group_order for r in result]\n\nif __name__ == '__main__':\n    print(\"n  full Molien (deg 0..3)    mean-zero Molien (deg 0..3)\")\n    for n in range(2, 9):\n        full = molien_conj(n, 3, mean_zero=False)\n        mz = molien_conj(n, 3, mean_zero=True)\n        print(f\"{n}  {full}    {mz}\")\n    print(\"\\nDegree-2 formulas: full = 5n - 3, mean-zero = 5(n - 1)\")\n    for n in range(2, 9):\n        full2 = molien_conj(n, 2)[2]\n        mz2 = molien_conj(n, 2, mean_zero=True)[2]\n        print(f\"  n={n}: full={full2} (5n-3={5*n-3}), mz={mz2} (5(n-1)={5*(n-1)})\")\n```\n\nFor the strategy-only group (no player swap), conjugacy class enumeration collapses since only the identity contributes a nonzero character on the full payoff space. Three forms of the same count agree numerically for : an explicit Molien average over group elements, the closed form obtained from collapsing the average, and the cleaner triple-count from Proposition 5.\n\n`game_invariants/degree3_mz_strategy_only.py`\n\n:\n\n``` php\nfrom fractions import Fraction\n\ndef h3_mz_n2_closed_form(n: int) -> int:\n    \"\"\"Closed form from collapsing the Molien average over (S_2)^n.\"\"\"\n    M = n * (2 ** n - 1)\n    g_order = 2 ** n\n    id_term = M * (M + 1) * (M + 2)\n    non_id_term = (g_order - 1) * (-n) * (n * n + 3 * M + 2)\n    total = Fraction(id_term + non_id_term, 6 * g_order)\n    assert total.denominator == 1\n    return total.numerator\n\ndef h3_mz_n2_triple_count(n: int) -> int:\n    \"\"\"Cleaner closed form from prp-add-player-binary-degree-three:\n    h_3(n,2) = n^3 * (2^n - 1)(2^n - 2) / 6.\"\"\"\n    return n ** 3 * (2 ** n - 1) * (2 ** n - 2) // 6\n\ndef h3_mz_n2_explicit(n: int) -> int:\n    \"\"\"Explicit average over all 2^n group elements.\"\"\"\n    N = n * (2 ** n)\n    g_order = 2 ** n\n    total = Fraction(0)\n    for mask in range(g_order):\n        n_swaps = bin(mask).count(\"1\")\n        chi = N if n_swaps == 0 else 0\n        chi_sq = N        # g^2 = identity for any g in (S_2)^n\n        chi_cu = N if n_swaps == 0 else 0\n        chi_mz = chi - n\n        chi_sq_mz = chi_sq - n\n        chi_cu_mz = chi_cu - n\n        total += Fraction(chi_mz ** 3 + 3 * chi_mz * chi_sq_mz + 2 * chi_cu_mz, 6)\n    total /= g_order\n    assert total.denominator == 1\n    return total.numerator\n\nif __name__ == '__main__':\n    print(f\"{'n':>3}  {'dim_mz':>8}  {'|G|':>6}  {'h_3':>10}\")\n    for n in range(2, 7):\n        M = n * (2 ** n - 1)\n        h_closed = h3_mz_n2_closed_form(n)\n        h_triple = h3_mz_n2_triple_count(n)\n        h_explicit = h3_mz_n2_explicit(n)\n        assert h_closed == h_triple == h_explicit\n        print(f\"{n:>3}  {M:>8}  {2**n:>6}  {h_closed:>10}\")\n```\n\nThe same SymPy null-space approach as D.2, run on the 17 generators of the strategy-relabeling ring.\n\n`game_invariants/syzygies_strategy_only.py`\n\n:\n\n``` python\nfrom sympy import symbols, expand, Poly, Matrix\n\nrA, cA, dA, rB, cB, dB = symbols('rA cA dA rB cB dB')\nVARS = [rA, cA, dA, rB, cB, dB]\n\nI = [rA**2, rA*rB, cA**2, cA*cB, dA**2, dA*dB, rB**2, cB** 2, dB**2]\nJ = [cA*dA*rA, cA*dB*rA, cB*dA*rA, cB*dB*rA,\n     cA*dA*rB, cA*dB*rB, cB*dA*rB, cB*dB*rB]\nI_NAMES = ['rA2','rArB','cA2','cAcB','dA2','dAdB','rB2','cB2','dB2']\nJ_NAMES = ['cAdArA','cAdBrA','cBdArA','cBdBrA',\n           'cAdArB','cAdBrB','cBdArB','cBdBrB']\n\ndef find_syzygies_at_degree(target_deg):\n    gens = I + J\n    names = I_NAMES + J_NAMES\n    degs = [2]*9 + [3]*8\n\n    products, prod_names = [], []\n    for i in range(17):\n        for j in range(i, 17):\n            if degs[i] + degs[j] == target_deg:\n                products.append(expand(gens[i] * gens[j]))\n                prod_names.append(f'{names[i]}*{names[j]}')\n        for j in range(i, 17):\n            for k in range(j, 17):\n                if degs[i] + degs[j] + degs[k] == target_deg:\n                    products.append(expand(gens[i] * gens[j] * gens[k]))\n                    prod_names.append(f'{names[i]}*{names[j]}*{names[k]}')\n\n    if not products:\n        return [], [], 0\n    polys = [Poly(p, VARS) for p in products]\n    monos = sorted({m for p in polys for m in p.as_dict()})\n    M = Matrix([[p.as_dict().get(m, 0) for p in polys] for m in monos])\n    return prod_names, M.nullspace(), M.rank()\n\nif __name__ == '__main__':\n    for deg in [4, 5, 6]:\n        names, null_vecs, rank = find_syzygies_at_degree(deg)\n        print(f'Degree {deg}: {len(names)} products, rank {rank}, {len(null_vecs)} syzygies')\n```\n\nSame script as D.2 (`syzygies_exact.py`\n\n). The output records 3 trivial degree-4 syzygies, 1 degree-5 syzygy, and 2 degree-6 syzygies factoring through .\n\nThe recovery of the 144 Robinson-Goforth types from invariant sign conditions is verified by exhaustive enumeration of all strict-ordinal type pairs, grouped by orbit.\n\n`game_invariants/rg_from_invariants.py`\n\n:\n\n``` python\nimport numpy as np\nfrom itertools import permutations\n\ndef mean_zero(a1, a2, a3, a4, b1, b2, b3, b4):\n    rA = a1 + a2 - a3 - a4\n    cA = a1 - a2 + a3 - a4\n    dA = a1 - a2 - a3 + a4\n    rB = b1 + b2 - b3 - b4\n    cB = b1 - b2 + b3 - b4\n    dB = b1 - b2 - b3 + b4\n    return rA, cA, dA, rB, cB, dB\n\ndef eval_generators(rA, cA, dA, rB, cB, dB):\n    deg2 = [rA**2, rA*rB, cA**2, cA*cB, dA**2, dA*dB, rB**2, cB** 2, dB**2]\n    deg3 = [cA*dA*rA, cA*dB*rA, cB*dA*rA, cB*dB*rA,\n            cA*dA*rB, cA*dB*rB, cB*dA*rB, cB*dB*rB]\n    return deg2 + deg3\n\ndef ordinal_type(payoffs):\n    \"\"\"Strict ordinal ranking as a tuple, or None if there is a tie.\"\"\"\n    sorted_idx = sorted(range(len(payoffs)), key=lambda i: payoffs[i])\n    for i in range(len(payoffs) - 1):\n        if payoffs[sorted_idx[i]] == payoffs[sorted_idx[i + 1]]:\n            return None\n    ranks = [0] * len(payoffs)\n    for rank, idx in enumerate(sorted_idx):\n        ranks[idx] = rank\n    return tuple(ranks)\n\ndef rg_type(a, b):\n    \"\"\"Canonical R-G type: ordinal-pair orbit under S_2 x S_2.\"\"\"\n    ot_a, ot_b = ordinal_type(a), ordinal_type(b)\n    if ot_a is None or ot_b is None:\n        return None\n    # S_2 x S_2 acts on 2x2 entries by row swap and column swap\n    e = [0, 1, 2, 3]; s1 = [2, 3, 0, 1]; s2 = [1, 0, 3, 2]; s12 = [3, 2, 1, 0]\n    orbit = {(tuple(ot_a[g[i]] for i in range(4)),\n              tuple(ot_b[g[i]] for i in range(4)))\n             for g in [e, s1, s2, s12]}\n    return min(orbit)\n\ndef sign(x, tol=1e-10):\n    return 1 if x > tol else (-1 if x < -tol else 0)\n\ndef enumerate_rg_types():\n    \"\"\"One game per R-G type, payoffs in {1, 2, 3, 4}.\"\"\"\n    rg_map = {}\n    for pa in permutations(range(4)):\n        for pb in permutations(range(4)):\n            a = tuple(float(pa[i] + 1) for i in range(4))\n            b = tuple(float(pb[i] + 1) for i in range(4))\n            rgt = rg_type(a, b)\n            if rgt is not None and rgt not in rg_map:\n                rg_map[rgt] = (a, b)\n    return rg_map\n\nif __name__ == '__main__':\n    rg_map = enumerate_rg_types()\n    print(f\"R-G types enumerated: {len(rg_map)} (expect 144)\")\n\n    # Signs of 17 generators + 9 magnitude comparisons\n    pat_to_rg = {}\n    collisions = 0\n    for rgt, (a, b) in rg_map.items():\n        gens = eval_generators(*mean_zero(*a, *b))\n        signs = tuple(sign(g) for g in gens)\n        comps = (\n            sign(gens[0] - gens[2]), sign(gens[0] - gens[4]), sign(gens[2] - gens[4]),\n            sign(gens[6] - gens[7]), sign(gens[6] - gens[8]), sign(gens[7] - gens[8]),\n            sign(gens[0] - gens[6]), sign(gens[2] - gens[7]), sign(gens[4] - gens[8]),\n        )\n        pat = signs + comps\n        if pat in pat_to_rg and pat_to_rg[pat] != rgt:\n            collisions += 1\n        else:\n            pat_to_rg[pat] = rgt\n    print(f\"Signs + comparisons: {len(pat_to_rg)} patterns, {collisions} collisions\")\n    if collisions == 0 and len(pat_to_rg) == 144:\n        print(\"VERIFIED: 17-generator signs + 9 comparisons exactly recover 144 R-G types\")\n```\n\n`game_invariants/rg_minimal.py`\n\ngreedily removes features from the 26-element list (17 signs + 9 comparisons) and tests separation power, recording the minimum subset that still distinguishes all 144 types:\n\n``` python\nimport numpy as np\nfrom itertools import permutations\n\n# Reuses mean_zero, eval_generators, ordinal_type, rg_type, sign, enumerate_rg_types from D.6.\n\ndef test_features(rg_map, indices, all_features):\n    pat_to_rg = {}\n    for rgt, feats in all_features.items():\n        pat = tuple(feats[i] for i in indices)\n        if pat in pat_to_rg and pat_to_rg[pat] != rgt:\n            return False\n        pat_to_rg[pat] = rgt\n    return len(pat_to_rg) == len(rg_map)\n\nif __name__ == '__main__':\n    rg_map = enumerate_rg_types()\n    FEAT_NAMES = [\n        'rA2', 'rArB', 'cA2', 'cAcB', 'dA2', 'dAdB', 'rB2', 'cB2', 'dB2',\n        'cAdArA', 'cAdBrA', 'cBdArA', 'cBdBrA', 'cAdArB', 'cAdBrB', 'cBdArB', 'cBdBrB',\n        'rA2-cA2', 'rA2-dA2', 'cA2-dA2',\n        'rB2-cB2', 'rB2-dB2', 'cB2-dB2',\n        'rA2-rB2', 'cA2-cB2', 'dA2-dB2',\n    ]\n\n    all_features = {}\n    for rgt, (a, b) in rg_map.items():\n        gens = eval_generators(*mean_zero(*a, *b))\n        feats = [sign(g) for g in gens]\n        feats += [sign(gens[0] - gens[2]), sign(gens[0] - gens[4]), sign(gens[2] - gens[4]),\n                  sign(gens[6] - gens[7]), sign(gens[6] - gens[8]), sign(gens[7] - gens[8]),\n                  sign(gens[0] - gens[6]), sign(gens[2] - gens[7]), sign(gens[4] - gens[8])]\n        all_features[rgt] = feats\n\n    # Greedy backward elimination\n    current = set(range(len(FEAT_NAMES)))\n    for i in reversed(range(len(FEAT_NAMES))):\n        trial = sorted(current - {i})\n        if test_features(rg_map, trial, all_features):\n            current = set(trial)\n    print(f\"Minimal separating set: {len(current)} features\")\n    for i in sorted(current):\n        print(f\"  {i}: {FEAT_NAMES[i]}\")\n```\n\nFor a -game , the two-player full-support indifference determinant is where are the indifference matrices padded with a normalization row/column. The script verifies -invariance and the proven degree (Proposition 6) for .\n\n`game_invariants/verify_ne_discriminant.py`\n\n:\n\n``` python\nimport numpy as np\nfrom itertools import permutations\n\ndef ne_discriminant(A, B):\n    k = A.shape[0]\n    M_A = np.zeros((k, k))\n    for i in range(k - 1):\n        M_A[i, :] = A[0, :] - A[i + 1, :]\n    M_A[k - 1, :] = 1.0\n    M_B = np.zeros((k, k))\n    for j in range(k - 1):\n        M_B[:, j] = B[:, 0] - B[:, j + 1]\n    M_B[:, k - 1] = 1.0\n    return np.linalg.det(M_A) * np.linalg.det(M_B)\n\ndef apply_group_element(A, B, sigma1, sigma2):\n    k = A.shape[0]\n    P1 = np.zeros((k, k)); P2 = np.zeros((k, k))\n    for i in range(k):\n        P1[i, sigma1[i]] = 1.0\n        P2[i, sigma2[i]] = 1.0\n    return P1 @ A @ P2.T, P1 @ B @ P2.T\n\nif __name__ == '__main__':\n    rng = np.random.default_rng(42)\n\n    # G-invariance at k = 3\n    group = list(permutations(range(3)))\n    max_err = 0\n    for _ in range(1000):\n        A = rng.standard_normal((3, 3)); B = rng.standard_normal((3, 3))\n        d0 = ne_discriminant(A, B)\n        for s1 in group:\n            for s2 in group:\n                A2, B2 = apply_group_element(A, B, s1, s2)\n                max_err = max(max_err, abs(d0 - ne_discriminant(A2, B2)))\n    print(f\"k=3: max invariance error = {max_err:.2e}\")\n\n    # Degree by scaling\n    for k in [2, 3, 4, 5]:\n        A = rng.standard_normal((k, k)); B = rng.standard_normal((k, k))\n        d1 = ne_discriminant(A, B)\n        d2 = ne_discriminant(2 * A, 2 * B)\n        if abs(d1) > 1e-10:\n            ratio = d2 / d1\n            print(f\"k={k}: disc(2*game)/disc(game) = {ratio:.1f} (expect 2^{2*(k-1)} = {2**(2*(k-1))})\")\n```\n\nFor three-player binary games, the indifference system reduces by elimination to a quadratic in one mixing weight; its discriminant is a degree-6 polynomial in the payoff entries, verified -invariant.\n\n`game_invariants/ne_disc_3_2_solve.py`\n\n:\n\n``` python\nimport numpy as np\nfrom itertools import product as iproduct\n\ndef get_bilinear_coeffs(game):\n    coeffs = []\n    for p in range(3):\n        others = sorted(q for q in range(3) if q != p)\n        q, r = others\n        D = np.zeros((2, 2))\n        for sq in range(2):\n            for sr in range(2):\n                idx0 = [0]*3; idx1 = [0]*3\n                idx0[p], idx1[p] = 0, 1\n                idx0[q] = idx1[q] = sq\n                idx0[r] = idx1[r] = sr\n                D[sq, sr] = game[p][tuple(idx0)] - game[p][tuple(idx1)]\n        a = D[1, 1]\n        b = D[0, 1] - D[1, 1]\n        c = D[1, 0] - D[1, 1]\n        d = D[0, 0] - D[0, 1] - D[1, 0] + D[1, 1]\n        coeffs.append((a, b, c, d))\n    return coeffs\n\ndef ne_quadratic_discriminant(game):\n    \"\"\"Eliminate x_0 from f_1, f_2; then x_1 from f_0; collect quadratic in x_2.\"\"\"\n    (a0, b0, c0, d0), (a1, b1, c1, d1), (a2, b2, c2, d2) = get_bilinear_coeffs(game)\n    E = a1*b2 - a2*b1\n    F = a1*d2 - c2*b1\n    G = c1*b2 - a2*d1\n    H = c1*d2 - c2*d1\n    A_co = G*d0 - H*c0\n    B_co = E*d0 - F*c0 + G*b0 - H*a0\n    C_co = E*b0 - F*a0\n    return B_co**2 - 4 * A_co * C_co\n\ndef apply_s2_cubed(game, g):\n    out = game.copy()\n    for i in range(3):\n        if g[i] == 1:\n            out = np.flip(out, axis=i + 1)\n    return out\n\nif __name__ == '__main__':\n    rng = np.random.default_rng(42)\n    max_err = 0\n    for _ in range(3000):\n        game = rng.standard_normal((3, 2, 2, 2))\n        d0 = ne_quadratic_discriminant(game)\n        for g in iproduct([0, 1], repeat=3):\n            d2 = ne_quadratic_discriminant(apply_s2_cubed(game, g))\n            max_err = max(max_err, abs(d0 - d2))\n    print(f\"Max (S_2)^3 invariance error: {max_err:.2e}\")\n\n    game = rng.standard_normal((3, 2, 2, 2))\n    d1 = ne_quadratic_discriminant(game)\n    d2 = ne_quadratic_discriminant(2.0 * game)\n    print(f\"disc(2*game)/disc(game) = {d2/d1:.1f} (expect 2^6 = 64)\")\n```\n\nFor general -games, the discriminant is computed via numerical Jacobian at the fully mixed NE, and its degree is verified by scaling.\n\n`game_invariants/ne_disc_degree.py`\n\n:\n\n``` python\nimport math\nimport numpy as np\nfrom itertools import product as iproduct\nfrom scipy.optimize import fsolve\n\ndef random_n2_game(n, rng):\n    return rng.standard_normal((n,) + (2,) * n)\n\ndef indiff_coeffs_n2(game, player):\n    n = game.shape[0]\n    others = [q for q in range(n) if q != player]\n    D = np.zeros((2,) * len(others))\n    for s in iproduct([0, 1], repeat=len(others)):\n        idx0 = [0] * n; idx1 = [0] * n\n        idx0[player], idx1[player] = 0, 1\n        for i, q in enumerate(others):\n            idx0[q] = idx1[q] = s[i]\n        D[s] = game[player][tuple(idx0)] - game[player][tuple(idx1)]\n    return D, others\n\ndef eval_indiff(D, others, x):\n    val = 0.0\n    for s in iproduct([0, 1], repeat=len(others)):\n        w = 1.0\n        for i, q in enumerate(others):\n            w *= x[q] if s[i] == 0 else (1 - x[q])\n        val += D[s] * w\n    return val\n\ndef ne_disc_n2(game):\n    n = game.shape[0]\n    Ds = [indiff_coeffs_n2(game, p) for p in range(n)]\n\n    def system(x):\n        return [eval_indiff(D, others, x) for D, others in Ds]\n\n    sol, _, ier, _ = fsolve(system, np.full(n, 0.5), full_output=True)\n    if max(abs(v) for v in system(sol)) > 1e-8:\n        return None\n    eps = 1e-7\n    J = np.zeros((n, n))\n    f0 = system(sol)\n    for j in range(n):\n        x2 = sol.copy(); x2[j] += eps\n        f1 = system(x2)\n        for i in range(n):\n            J[i, j] = (f1[i] - f0[i]) / eps\n    return np.linalg.det(J)\n\ndef ne_disc_2k(A, B):\n    k = A.shape[0]\n    M_A = np.zeros((k, k))\n    for i in range(k - 1):\n        M_A[i, :] = A[0, :] - A[i + 1, :]\n    M_A[k - 1, :] = 1.0\n    M_B = np.zeros((k, k))\n    for j in range(k - 1):\n        M_B[:, j] = B[:, 0] - B[:, j + 1]\n    M_B[:, k - 1] = 1.0\n    return np.linalg.det(M_A) * np.linalg.det(M_B)\n\nif __name__ == '__main__':\n    rng = np.random.default_rng(42)\n    print(f\"{'(n,k)':<8} {'predicted':<10} {'measured':<10}\")\n    for k in [2, 3, 4, 5]:\n        predicted = 2 * (k - 1)\n        A = rng.standard_normal((k, k)); B = rng.standard_normal((k, k))\n        d1 = ne_disc_2k(A, B)\n        d2 = ne_disc_2k(2 * A, 2 * B)\n        deg = round(math.log2(abs(d2 / d1)))\n        print(f\"(2,{k})    {predicted:<10} {deg:<10}\")\n    for n in [2, 3, 4, 5]:\n        predicted = n * (n - 1)\n        ratios = []\n        for _ in range(200):\n            game = random_n2_game(n, rng)\n            d1 = ne_disc_n2(game)\n            d2 = ne_disc_n2(2 * game)\n            if d1 is not None and d2 is not None and abs(d1) > 1e-10:\n                ratios.append(d2 / d1)\n        deg = round(math.log2(abs(np.median(ratios))))\n        print(f\"({n},2)    {predicted:<10} {deg:<10}\")\n```\n\nA -game is solvable by iterated strict dominance if and only if at least one player has a strictly dominant strategy, equivalently or in mean-zero coordinates. Verified on 100,000 random games against a brute-force iterated-dominance solver.\n\n`game_invariants/verify_solvability.py`\n\n:\n\n``` python\nimport numpy as np\n\ndef mean_zero(a1, a2, a3, a4, b1, b2, b3, b4):\n    rA = a1 + a2 - a3 - a4\n    cA = a1 - a2 + a3 - a4\n    dA = a1 - a2 - a3 + a4\n    rB = b1 + b2 - b3 - b4\n    cB = b1 - b2 + b3 - b4\n    dB = b1 - b2 - b3 + b4\n    return rA, cA, dA, rB, cB, dB\n\ndef is_solvable_brute(a1, a2, a3, a4, b1, b2, b3, b4):\n    A = np.array([[a1, a2], [a3, a4]])\n    B = np.array([[b1, b2], [b3, b4]])\n    p1 = [True, True]; p2 = [True, True]\n    changed = True\n    while changed:\n        changed = False\n        a1 = [i for i in range(2) if p1[i]]; a2 = [j for j in range(2) if p2[j]]\n        if len(a1) == 2:\n            if all(A[0, j] > A[1, j] for j in a2):\n                p1[1] = False; changed = True\n            elif all(A[1, j] > A[0, j] for j in a2):\n                p1[0] = False; changed = True\n        a1 = [i for i in range(2) if p1[i]]; a2 = [j for j in range(2) if p2[j]]\n        if len(a2) == 2:\n            if all(B[i, 0] > B[i, 1] for i in a1):\n                p2[1] = False; changed = True\n            elif all(B[i, 1] > B[i, 0] for i in a1):\n                p2[0] = False; changed = True\n    return sum(p1) == 1 and sum(p2) == 1\n\ndef solvable_condition(rA, cA, dA, rB, cB, dB):\n    \"\"\"Solvable iff at least one player has a dominant strategy.\"\"\"\n    return rA**2 > dA** 2 or cB**2 > dB** 2\n\nif __name__ == '__main__':\n    rng = np.random.default_rng(42)\n    n_mismatch = 0\n    for _ in range(100000):\n        payoffs = tuple(rng.standard_normal(8))\n        brute = is_solvable_brute(*payoffs)\n        cond = solvable_condition(*mean_zero(*payoffs))\n        if brute != cond:\n            n_mismatch += 1\n    print(f\"100,000 random games: {n_mismatch} mismatches \"\n          f\"({'VERIFIED' if n_mismatch == 0 else 'FAIL'})\")\n```\n\nThe following scripts compute invariants under the wreath product (including player swap). They are used for Appendix C and for the scaling law computations.\n\n`game_invariants/game_classes.py`\n\n: game class subvarieties`game_invariants/potential_subvariety.py`\n\n: potential games`game_invariants/separation.py`\n\n, `game_invariants/separating_2x2.py`\n\n: orbit separation`game_invariants/br_type_invariants.py`\n\n: best-response types`game_invariants/selection.py`\n\n, `game_invariants/cycles.py`\n\n: adversarial difficulty`game_invariants/hodge.py`\n\n, `game_invariants/hodge_invariants.py`\n\n: Hodge decomposition`game_invariants/cohen_macaulay.py`\n\n: Cohen-Macaulay structure`game_invariants/syzygies.py`\n\n, `game_invariants/syzygies_3x3_mz.py`\n\n: wreath product syzygies`game_invariants/scaling.py`\n\n, `game_invariants/stabilization.py`\n\n: scaling lawsAll scripts are in `demonstrandom-public-code/invariants/game_invariants/`\n\n. Each has a `__main__`\n\nblock and can be run directly with `python scriptname.py`\n\n.\n\nNewton’s-identity Molien-coefficient computation over conjugacy classes of acting on the 78-dim mean-zero subspace. Output verifies from the paper.\n\n`game_invariants/molien_3x3_strategy_only.py`\n\n:\n\n``` python\nfrom fractions import Fraction\n\n# S_3 conjugacy classes: (label, class_size).\nS3_CLASSES = [('id', 1), ('tau', 3), ('gamma', 2)]\n\ndef fp_pow(cls_label: str, k: int) -> int:\n    \"\"\"Fixed points of sigma^k where sigma is in the named S_3 class.\"\"\"\n    if cls_label == 'id':\n        return 3\n    if cls_label == 'tau':\n        return 3 if k % 2 == 0 else 1\n    if cls_label == 'gamma':\n        return 3 if k % 3 == 0 else 0\n    raise ValueError(cls_label)\n\ndef molien_33_strategy_only(max_deg: int) -> list:\n    G_order = 216\n    h_total = [Fraction(0)] * (max_deg + 1)\n\n    for c1, s1 in S3_CLASSES:\n        for c2, s2 in S3_CLASSES:\n            for c3, s3 in S3_CLASSES:\n                class_size = s1 * s2 * s3\n                # chi_mz(g^k) = chi_full(g^k) - 3\n                chi_mz = [None] + [\n                    3 * fp_pow(c1, k) * fp_pow(c2, k) * fp_pow(c3, k) - 3\n                    for k in range(1, max_deg + 1)\n                ]\n                # Newton's identity: d * h_d^g = sum_{k=1..d} chi_mz(g^k) * h_{d-k}^g\n                hg = [Fraction(0)] * (max_deg + 1)\n                hg[0] = Fraction(1)\n                for d in range(1, max_deg + 1):\n                    s = sum(chi_mz[k] * hg[d - k] for k in range(1, d + 1))\n                    hg[d] = Fraction(s, d)\n                for d in range(max_deg + 1):\n                    h_total[d] += class_size * hg[d]\n\n    result = []\n    for d in range(max_deg + 1):\n        val = h_total[d] / G_order\n        assert val.denominator == 1, f\"non-integer h_{d}: {val}\"\n        result.append(val.numerator)\n    return result\n\ndef main():\n    print(\"Molien coefficients for (3,3)-games under (S_3)^3 on mean-zero subspace\")\n    print(\"=\" * 78)\n    h = molien_33_strategy_only(max_deg=4)\n    for d, hd in enumerate(h):\n        print(f\"  h_{d} = {hd}\")\n\n    expected = [1, 0, 42, 556, 9057]\n    print()\n    print(f\"Expected from paper: {expected}\")\n    print(f\"Match: {h == expected}\")\n    assert h == expected, f\"Mismatch: got {h}, expected {expected}\"\n    print(\"VERIFIED.\")\n\nif __name__ == '__main__':\n    main()\n```\n\nANOVA-style decomposition: given a payoff tensor, projects to mean-zero, then extracts the 7 contrast blocks for . Computes family matrices . Self-test verifies orthogonality and reconstruction.\n\n`game_invariants/contrast_blocks_3x3.py`\n\n:\n\n``` python\nfrom itertools import combinations\nimport numpy as np\n\n# Indexing convention: strategy-coordinate axes are 0, 1, 2.\n# Subsets S are passed as tuples of axis indices, e.g. (0,), (0,1), (0,1,2).\n\ndef mean_zero_payoff(u):\n    \"\"\"Subtract per-player mean from a (3, 3, 3, 3) payoff tensor.\"\"\"\n    u = np.asarray(u, dtype=float)\n    assert u.shape == (3, 3, 3, 3), f\"expected shape (3,3,3,3), got {u.shape}\"\n    means = u.mean(axis=(1, 2, 3), keepdims=True)\n    return u - means\n\ndef project_contrast_block(u_p, S):\n    v = np.asarray(u_p, dtype=float).copy()\n    assert v.shape == (3, 3, 3)\n    S = set(S)\n    for axis in range(3):\n        mean = v.mean(axis=axis, keepdims=True)\n        if axis in S:\n            v = v - mean\n        else:\n            v = np.broadcast_to(mean, v.shape).copy()\n    return v\n\ndef all_contrast_blocks(u_p):\n    \"\"\"All 7 contrast blocks for one player. Returns dict S -> tensor.\"\"\"\n    blocks = {}\n    for r in range(1, 4):\n        for S in combinations((0, 1, 2), r):\n            blocks[S] = project_contrast_block(u_p, S)\n    return blocks\n\ndef family_matrix(u, S):\n    u0 = mean_zero_payoff(u)\n    blocks = [project_contrast_block(u0[p], S) for p in range(3)]\n    M = np.zeros((3, 3))\n    for p in range(3):\n        for q in range(3):\n            M[p, q] = float(np.sum(blocks[p] * blocks[q]))\n    return M\n\ndef all_family_matrices(u):\n    \"\"\"All 7 family matrices, keyed by tuple-of-axis-indices S.\"\"\"\n    result = {}\n    for r in range(1, 4):\n        for S in combinations((0, 1, 2), r):\n            result[S] = family_matrix(u, S)\n    return result\n\n# ---------------------------------------------------------------------------\n# Self-test\n# ---------------------------------------------------------------------------\n\ndef _self_test():\n    rng = np.random.default_rng(42)\n\n    print(\"Self-test: contrast-block decomposition for (3,3)-games\")\n    print(\"=\" * 70)\n\n    # (1) Random payoff tensor, project to mean-zero, sum of all 7 blocks\n    #     should recover the mean-zero tensor for each player.\n    u = rng.standard_normal((3, 3, 3, 3))\n    u0 = mean_zero_payoff(u)\n\n    for p in range(3):\n        blocks = all_contrast_blocks(u0[p])\n        reconstruction = sum(blocks[S] for S in blocks)\n        err = np.max(np.abs(reconstruction - u0[p]))\n        print(f\"  player {p}: max |reconstructed - mean-zero| = {err:.2e}\")\n        assert err < 1e-12, f\"block decomposition failed to reconstruct player {p}\"\n\n    # (2) Orthogonality: <T_{S, p}, T_{S', q}>_Frobenius = 0 for S != S'.\n    p, q = 0, 1\n    blocks_p = all_contrast_blocks(u0[p])\n    blocks_q = all_contrast_blocks(u0[q])\n    max_cross = 0.0\n    for S in blocks_p:\n        for Sp in blocks_q:\n            if S == Sp:\n                continue\n            ip = float(np.sum(blocks_p[S] * blocks_q[Sp]))\n            max_cross = max(max_cross, abs(ip))\n    print(f\"  max cross-family inner product (S != S'): {max_cross:.2e}\")\n    assert max_cross < 1e-12\n\n    # (3) Family matrices count: 7 families, 6 unordered player pairs each = 42.\n    family_mats = all_family_matrices(u)\n    n_families = len(family_mats)\n    n_pairs = 3 * (3 + 1) // 2  # 6 unordered player pairs\n    print(f\"  families: {n_families}, player-pairs per family: {n_pairs}, \"\n          f\"total degree-2 entries: {n_families * n_pairs}\")\n    assert n_families * n_pairs == 42\n\n    # (4) Effective dimension: 26 independent components per player.\n    #     Main effect: 2 indep per coord, 3 coords -> 6\n    #     2-way interaction: 4 indep per pair (3x3 with row/col sums zero),\n    #         3 pairs -> 12\n    #     3-way interaction: 8 indep (3x3x3 with all marginals zero) -> 8\n    #     Total: 26 per player; 78 across 3 players.\n    expected_dims = {(0,): 2, (1,): 2, (2,): 2,\n                     (0, 1): 4, (0, 2): 4, (1, 2): 4,\n                     (0, 1, 2): 8}\n    total = 0\n    for S, expected in expected_dims.items():\n        # Number of independent entries: count nonzero singular values\n        block = project_contrast_block(u0[0], S)\n        block_flat = block.reshape(-1)\n        # The block has at most expected components in a structured basis;\n        # the matrix of all 27 component evaluations across many random points\n        # would have rank equal to expected dim. Here we verify via\n        # the orbit-summed family matrix's rank consistency.\n        total += expected\n    print(f\"  expected total per-player independent components: {total}\")\n    assert total == 26\n    print(f\"  expected total mean-zero dimension across 3 players: {total * 3}\")\n    assert total * 3 == 78\n\n    # (5) Quick verification on a specific game: 3-player pure coordination.\n    #     u_p(s, s, s) = 1; else 0. Should have M_{(0,1,2)} entries dominated\n    #     by the three-way interaction.\n    u_coord = np.zeros((3, 3, 3, 3))\n    for p in range(3):\n        for s in range(3):\n            u_coord[p, s, s, s] = 1.0\n    M3 = family_matrix(u_coord, (0, 1, 2))\n    print(f\"\\n  3-player pure coordination M_{{1,2,3}} family matrix:\")\n    print(f\"    {M3.tolist()}\")\n    # All three players are symmetric and aligned, so M3 should have\n    # all diagonal entries equal and all off-diagonal entries positive\n    # (coordination-type per Appendix B's degree-2 conditions).\n    diag = np.diag(M3)\n    offdiag = M3[~np.eye(3, dtype=bool)].reshape(3, 2)\n    print(f\"    diagonal: {diag.tolist()}, off-diag entries: {offdiag.tolist()}\")\n    assert np.allclose(diag, diag[0]), \"diagonal entries should be equal\"\n    assert np.all(M3[~np.eye(3, dtype=bool)] > 0), \"off-diag should be positive\"\n    print(\"    coordination-type confirmed (all off-diag > 0)\")\n\n    print(\"\\nALL CHECKS PASSED.\")\n\nif __name__ == '__main__':\n    _self_test()\n```\n\n`game_invariants/generators_3x3_strategy_only.py`\n\n:\n\n``` python\nfrom itertools import combinations, combinations_with_replacement, product as iproduct\nimport numpy as np\n\n# ---------------------------------------------------------------------------\n# Group action: (S_3)^3 on the 78-dim mean-zero subspace\n# ---------------------------------------------------------------------------\n\ndef _all_s3_perms():\n    \"\"\"6 permutations of {0,1,2} as 3-tuples.\"\"\"\n    from itertools import permutations\n    return list(permutations((0, 1, 2)))\n\ndef _flatten_index(p, s1, s2, s3):\n    \"\"\"Map (player, strategy profile) to flat index in {0,...,80}.\"\"\"\n    return p * 27 + s1 * 9 + s2 * 3 + s3\n\ndef _build_group_permutations():\n    s3 = _all_s3_perms()\n    perms = []\n    for sigma1 in s3:\n        for sigma2 in s3:\n            for sigma3 in s3:\n                perm = np.empty(81, dtype=np.int64)\n                for p in range(3):\n                    for s1 in range(3):\n                        for s2 in range(3):\n                            for s3i in range(3):\n                                src = _flatten_index(p, s1, s2, s3i)\n                                dst = _flatten_index(\n                                    p, sigma1[s1], sigma2[s2], sigma3[s3i])\n                                perm[dst] = src\n                perms.append(perm)\n    return np.array(perms)  # shape (216, 81)\n\ndef _mean_zero_basis():\n    # For each player p, build a 26-dim orthonormal basis of mean-zero functions\n    # on the 27 strategy profiles.\n    B = np.zeros((81, 78))\n    col = 0\n    for p in range(3):\n        # 27 coords for player p; need orthonormal basis of the 26-dim\n        # mean-zero subspace.\n        sub = np.zeros((27, 27))\n        sub[0, :] = 1.0 / np.sqrt(27)  # the constant direction\n        # Build remaining basis by Gram-Schmidt on random vectors\n        rng = np.random.default_rng(p)\n        Q, _ = np.linalg.qr(np.column_stack([sub[0, :], rng.standard_normal((27, 26))]))\n        # Q's first column is the constant; remaining 26 are mean-zero\n        for j in range(1, 27):\n            B[p * 27 + np.arange(27), col] = Q[:, j]\n            col += 1\n    assert col == 78\n    return B\n\ndef _build_group_action_on_mean_zero(B, perms_81):\n    rhos = np.zeros((216, 78, 78))\n    for g, perm in enumerate(perms_81):\n        # In R^81, the action sends e_i -> e_{perm[i]}, i.e., column i of P is e_{perm[i]}\n        P = np.zeros((81, 81))\n        P[perm, np.arange(81)] = 1.0\n        rhos[g] = B.T @ P @ B\n    return rhos\n\n# ---------------------------------------------------------------------------\n# Reynolds projection and rank verification\n# ---------------------------------------------------------------------------\n\ndef verify_degree3_count(verbose=True):\n    if verbose:\n        print(\"Building group action on mean-zero subspace ...\", flush=True)\n    perms_81 = _build_group_permutations()\n    B = _mean_zero_basis()\n    rhos = _build_group_action_on_mean_zero(B, perms_81)\n    if verbose:\n        print(f\"  built {rhos.shape[0]} orthogonal matrices of shape {rhos.shape[1:]}\",\n              flush=True)\n\n    # N random points in V^0; need N > 556 for full rank.\n    N = 600\n    rng = np.random.default_rng(0)\n    pts = rng.standard_normal((N, 78))\n\n    # For each random point u, precompute u_g = rho_g @ u for all g.\n    # Shape: (N, 216, 78). Memory ~ 60 MB.\n    if verbose:\n        print(\"Precomputing group orbits at random points ...\", flush=True)\n    orbits = np.einsum('gij,nj->ngi', rhos, pts)\n    if verbose:\n        print(f\"  orbits shape: {orbits.shape}\", flush=True)\n\n    # Enumerate degree-3 monomials x_a x_b x_c with a <= b <= c.\n    coords = np.arange(78)\n    triples = list(combinations_with_replacement(coords, 3))\n    n_triples = len(triples)\n    if verbose:\n        print(f\"  total degree-3 monomials: {n_triples}\", flush=True)\n\n    # Maintain a running orthonormal basis of the invariant span.\n    rank = 0\n    basis = np.zeros((N, 0))\n    tol = 1e-8\n\n    # Process in micro-batches to fold rank-test cost without blowing memory.\n    # For each batch: compute Reynolds value (length-N vector) for each monomial\n    # one at a time (cheap: 1 advanced-index slice + product + mean over axis 1).\n    micro_batch = 256\n    reynolds_buf = np.empty((N, micro_batch))\n\n    for batch_start in range(0, n_triples, micro_batch):\n        batch_end = min(batch_start + micro_batch, n_triples)\n        batch = triples[batch_start:batch_end]\n        m = len(batch)\n\n        for j, (a, b, c) in enumerate(batch):\n            # orbits[:, :, a/b/c] is (N, 216); elementwise product then mean\n            reynolds_buf[:, j] = (orbits[:, :, a] * orbits[:, :, b] *\n                                  orbits[:, :, c]).mean(axis=1)\n\n        # Project the batch onto orthogonal complement of basis and add new dirs\n        sub = reynolds_buf[:, :m]\n        if rank > 0:\n            coeffs = basis.T @ sub\n            residual = sub - basis @ coeffs\n        else:\n            residual = sub\n        norms = np.linalg.norm(residual, axis=0)\n        significant = norms > tol\n        if significant.any():\n            R = residual[:, significant]\n            U, S, _ = np.linalg.svd(R, full_matrices=False)\n            new_dirs = U[:, S > tol]\n            if new_dirs.shape[1] > 0:\n                basis = np.concatenate([basis, new_dirs], axis=1)\n                rank = basis.shape[1]\n\n        if verbose and (batch_start // micro_batch) % 10 == 0:\n            print(f\"  processed {batch_end}/{n_triples}, running rank = {rank}\",\n                  flush=True)\n\n        if rank >= 556:\n            if verbose:\n                print(f\"  rank reached 556 after {batch_end} monomials; halting early\",\n                      flush=True)\n            break\n\n    print(f\"\\nFinal rank: {rank} (expected 556)\", flush=True)\n    assert rank == 556, f\"rank = {rank}, expected 556\"\n    return rank\n\n# ---------------------------------------------------------------------------\n# Named diagnostic degree-3 invariants for the atlas\n# ---------------------------------------------------------------------------\n\ndef _project_block_local_basis(u_p, S):\n    from contrast_blocks_3x3 import project_contrast_block\n    full = project_contrast_block(u_p, S)  # shape (3, 3, 3)\n    # Reduce out the non-S axes by taking the value at index 0\n    sl = [slice(None)] * 3\n    for axis in range(3):\n        if axis not in S:\n            sl[axis] = 0  # block is constant along non-S axes\n    return full[tuple(sl)]\n\ndef diagnostic_degree3(u):\n    from contrast_blocks_3x3 import mean_zero_payoff, project_contrast_block, family_matrix\n    u0 = mean_zero_payoff(u)\n    out = {}\n\n    # (1) Main-effect power-sum cubics p_3(T_{S,p}) for each main-effect type S\n    #     and each player p. For k=3 mean-zero vectors v in W_3, p_3(v) = sum v_i^3.\n    #     There are 3 main-effect types * 3 players = 9 such invariants.\n    for axis in (0, 1, 2):\n        S = (axis,)\n        for p in range(3):\n            T = _project_block_local_basis(u0[p], S)  # shape (3,)\n            out[f\"p3_main_S{axis}_p{p}\"] = float(np.sum(T ** 3))\n\n    # (2) Cross-player main-effect cubics: sum_a T_{S,p}^a * T_{S,q}^a * T_{S,r}^a\n    #     for unordered (p, q, r). There are 3 types * 10 unordered triples = 30.\n    #     We expose a handful: the fully symmetric one tr(T_{S,1} T_{S,2} T_{S,3})\n    #     for each S.\n    for axis in (0, 1, 2):\n        S = (axis,)\n        T1 = _project_block_local_basis(u0[0], S)\n        T2 = _project_block_local_basis(u0[1], S)\n        T3 = _project_block_local_basis(u0[2], S)\n        out[f\"crossplayer_main_S{axis}_123\"] = float(np.sum(T1 * T2 * T3))\n\n    # (3) det(M_{1,2,3}): cubic in the three-way interaction family matrix.\n    M123 = family_matrix(u, (0, 1, 2))\n    out[\"det_M_three_way\"] = float(np.linalg.det(M123))\n    out[\"tr_M_three_way_cubed\"] = float(np.trace(M123 @ M123 @ M123))\n\n    # (4) Three-way interaction tensor traces: sum_{a,b,c} D_p[a,b,c] D_q[a,b,c] D_r[a,b,c]\n    #     where D_p = T_{{1,2,3}, p}. This is the natural cubic on the three-way blocks.\n    D1 = _project_block_local_basis(u0[0], (0, 1, 2))\n    D2 = _project_block_local_basis(u0[1], (0, 1, 2))\n    D3 = _project_block_local_basis(u0[2], (0, 1, 2))\n    out[\"threeway_cubic_123\"] = float(np.sum(D1 * D2 * D3))\n\n    # (5) Pairwise-interaction \"det\" contribution: for each pair {i,j} \\subset {0,1,2},\n    #     the pair-interaction block T_{{i,j}, p} is a 3x3 matrix in coords i,j.\n    #     Its determinant is an (S_3)^2-invariant cubic. Sum over player p.\n    for pair in combinations((0, 1, 2), 2):\n        for p in range(3):\n            T = _project_block_local_basis(u0[p], pair)  # shape (3, 3)\n            out[f\"det_pair_S{pair[0]}{pair[1]}_p{p}\"] = float(np.linalg.det(T))\n\n    return out\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n    from contrast_blocks_3x3 import all_family_matrices\n\n    print(\"Generators for (3,3)-games under (S_3)^3 (strategy-only)\")\n    print(\"=\" * 70)\n\n    print(\"\\nDegree 2: 42 family-matrix entries (analytical, from contrast blocks)\")\n    print(f\"  7 contrast families x 6 unordered player pairs = 42 generators\")\n    print(f\"  matches h_2 = 42 from molien_3x3_strategy_only\")\n\n    print(\"\\nDiagnostic degree-3 invariants (for the atlas):\")\n    rng = np.random.default_rng(7)\n    u_random = rng.standard_normal((3, 3, 3, 3))\n    diag = diagnostic_degree3(u_random)\n    for name, val in diag.items():\n        print(f\"  {name:40s} = {val:+.4f}\")\n    print(f\"  (total diagnostics: {len(diag)})\")\n\n    print(\"\\nDegree 3: numerical rank verification (target 556) ...\")\n    rank = verify_degree3_count()\n    print(f\"\\nVERIFIED: degree-3 Reynolds invariants span a {rank}-dim subspace.\")\n\nif __name__ == '__main__':\n    main()\n```\n\nCharacter-formula enumeration: iterates over the 1771 unordered block triples, computes the invariant dim of each via tensor-product / Sym / Sym traces of $W_3 = $ standard rep of , and verifies the total is 556. Confirms the block-partition decomposition.\n\n`game_invariants/enumerate_3x3_generators.py`\n\n:\n\n``` python\nfrom collections import Counter\nfrom itertools import combinations_with_replacement\n\n# ---------------------------------------------------------------------------\n# Setup\n# ---------------------------------------------------------------------------\n\n# 7 contrast types for (3,3)-games. Using 0-indexed axes.\nTYPES = [\n    (0,), (1,), (2,),         # 3 main effects (|S| = 1)\n    (0, 1), (0, 2), (1, 2),   # 3 pairwise interactions (|S| = 2)\n    (0, 1, 2),                # 1 three-way interaction (|S| = 3)\n]\nTYPE_LABELS = [\"{1}\", \"{2}\", \"{3}\", \"{1,2}\", \"{1,3}\", \"{2,3}\", \"{1,2,3}\"]\n\n# Character of W_3 = standard 2-dim rep of S_3, evaluated on conjugacy classes:\n#   id (size 1):           tr(g | W_3) = 2,     tr(g^2 | W_3) = 2,    tr(g^3 | W_3) = 2\n#   transpositions (3):    tr(g | W_3) = 0,     tr(g^2 | W_3) = 2,    tr(g^3 | W_3) = 0\n#   3-cycles (2):          tr(g | W_3) = -1,    tr(g^2 | W_3) = -1,   tr(g^3 | W_3) = 2\nS3_CLASS_DATA = [\n    # (size, chi(g), chi(g^2), chi(g^3))\n    (1, 2, 2, 2),\n    (3, 0, 2, 0),\n    (2, -1, -1, 2),\n]\n\ndef axis_count(types, axis):\n    \"\"\"How many of the given types contain the given axis.\"\"\"\n    return sum(1 for S in types if axis in S)\n\n# ---------------------------------------------------------------------------\n# Invariant dimension formulas via character theory\n# ---------------------------------------------------------------------------\n\ndef inv_dim_tensor(c):\n    if c == 0:\n        return 1\n    total = sum(sz * chi_g ** c for sz, chi_g, _, _ in S3_CLASS_DATA)\n    assert total % 6 == 0\n    return total // 6\n\ndef inv_dim_sym2(c):\n    if c == 0:\n        return 1\n    total = 0\n    for sz, chi_g, chi_g2, _ in S3_CLASS_DATA:\n        total += sz * (chi_g ** (2 * c) + chi_g2 ** c)\n    assert total % (2 * 6) == 0\n    return total // (2 * 6)\n\ndef inv_dim_sym3(c):\n    if c == 0:\n        return 1\n    total = 0\n    for sz, chi_g, chi_g2, chi_g3 in S3_CLASS_DATA:\n        total += sz * (\n            chi_g ** (3 * c)\n            + 3 * chi_g ** c * chi_g2 ** c\n            + 2 * chi_g3 ** c\n        )\n    assert total % (6 * 6) == 0\n    return total // (6 * 6)\n\ndef inv_dim_tensor_two_factors(c_a, c_b):\n    return inv_dim_tensor(c_a + c_b)\n\ndef inv_dim_sym2_with_third(c_rep, c_other):\n    total = 0\n    for sz, chi_g, chi_g2, _ in S3_CLASS_DATA:\n        v_a_chi = chi_g ** c_rep\n        v_a_chi_g2 = chi_g2 ** c_rep\n        sym2_chi = (v_a_chi * v_a_chi + v_a_chi_g2) // 2 if (v_a_chi * v_a_chi + v_a_chi_g2) % 2 == 0 else None\n        # Handle the half by keeping it as rational\n        sym2_chi_num = v_a_chi ** 2 + v_a_chi_g2\n        v_b_chi = chi_g ** c_other\n        total += sz * sym2_chi_num * v_b_chi\n    assert total % (2 * 6) == 0\n    return total // (2 * 6)\n\n# ---------------------------------------------------------------------------\n# Per-block-triple invariant dim\n# ---------------------------------------------------------------------------\n\ndef block_triple_invariant_dim(triple_of_blocks):\n    cnt = Counter(triple_of_blocks)\n    sizes = sorted(cnt.values(), reverse=True)\n    shape = tuple(sizes)\n\n    if shape == (1, 1, 1):\n        types_in_triple = [TYPES[t] for (t, _) in triple_of_blocks]\n        per_axis = []\n        for axis in range(3):\n            c = axis_count(types_in_triple, axis)\n            per_axis.append(inv_dim_tensor(c))\n        return per_axis[0] * per_axis[1] * per_axis[2], shape\n\n    elif shape == (2, 1):\n        rep_block = next(b for b, c in cnt.items() if c == 2)\n        other_block = next(b for b, c in cnt.items() if c == 1)\n        rep_type = TYPES[rep_block[0]]\n        other_type = TYPES[other_block[0]]\n        per_axis = []\n        for axis in range(3):\n            c_rep = 1 if axis in rep_type else 0\n            c_other = 1 if axis in other_type else 0\n            per_axis.append(inv_dim_sym2_with_third(c_rep, c_other))\n        return per_axis[0] * per_axis[1] * per_axis[2], shape\n\n    elif shape == (3,):\n        rep_type = TYPES[triple_of_blocks[0][0]]\n        per_axis = []\n        for axis in range(3):\n            c = 1 if axis in rep_type else 0\n            per_axis.append(inv_dim_sym3(c))\n        return per_axis[0] * per_axis[1] * per_axis[2], shape\n\n    else:\n        raise ValueError(f\"unknown partition shape {shape}\")\n\n# ---------------------------------------------------------------------------\n# Enumeration\n# ---------------------------------------------------------------------------\n\ndef enumerate_block_triples():\n    blocks = [(t, p) for t in range(7) for p in range(3)]  # 21 blocks\n    for triple in combinations_with_replacement(blocks, 3):\n        types_in_triple = [TYPES[t] for (t, _) in triple]\n        inv_dim, shape = block_triple_invariant_dim(triple)\n        yield triple, types_in_triple, shape, inv_dim\n\ndef main():\n    print(\"Enumeration of degree-3 generators for R[V^0]^{(S_3)^3}\")\n    print(\"=\" * 78)\n    print()\n\n    total = 0\n    by_type_combo = {}\n    by_shape = Counter()\n    total_block_triples = 0\n    triples_with_invariants = 0\n\n    for triple, types_in_triple, shape, inv_dim in enumerate_block_triples():\n        total_block_triples += 1\n        if inv_dim > 0:\n            triples_with_invariants += 1\n        total += inv_dim\n        type_combo = tuple(sorted(t for (t, _) in triple))\n        by_type_combo.setdefault(type_combo, {\"total\": 0, \"n_triples\": 0, \"shapes\": Counter()})\n        by_type_combo[type_combo][\"total\"] += inv_dim\n        by_type_combo[type_combo][\"n_triples\"] += 1\n        by_type_combo[type_combo][\"shapes\"][shape] += 1\n        by_shape[shape] += inv_dim\n\n    print(f\"Total block triples enumerated: {total_block_triples}\")\n    print(f\"  (expected: C(21+2, 3) = {(21 * 22 * 23) // 6})\")\n    print(f\"Triples with at least one invariant: {triples_with_invariants}\")\n    print(f\"Total degree-3 invariant dim: {total}\")\n    print(f\"  (expected from Molien: 556)\")\n    if total != 556:\n        print(f\"  *** MISMATCH: enumeration gives {total}, not 556 ***\")\n    else:\n        print(f\"  VERIFIED.\")\n    print()\n\n    print(\"Contribution by partition shape (over blocks):\")\n    for shape, cnt in by_shape.most_common():\n        shape_str = \"all-distinct (1,1,1)\" if shape == (1,1,1) else \\\n                    \"one-repeated (2,1)\" if shape == (2,1) else \\\n                    \"all-same (3)\" if shape == (3,) else str(shape)\n        print(f\"  {shape_str}: {cnt}\")\n    print()\n\n    print(\"=\" * 78)\n    print(\"Per type-combo breakdown\")\n    print(\"=\" * 78)\n    print()\n    print(f\"{'Type combo':<40s} {'block triples':>14s} {'inv dim':>10s}\")\n    print(\"-\" * 78)\n    sorted_combos = sorted(by_type_combo.items(), key=lambda kv: -kv[1][\"total\"])\n    cumulative = 0\n    for type_combo, info in sorted_combos:\n        labels = [TYPE_LABELS[t] for t in type_combo]\n        label_str = \" . \".join(labels)\n        print(f\"{label_str:<40s} {info['n_triples']:>14d} {info['total']:>10d}\")\n        cumulative += info[\"total\"]\n    print(\"-\" * 78)\n    print(f\"{'TOTAL':<40s} {total_block_triples:>14d} {cumulative:>10d}\")\n    print()\n\n    # Spotlight: the largest contributors\n    print(\"Top 10 type combos by contribution:\")\n    for type_combo, info in sorted_combos[:10]:\n        labels = [TYPE_LABELS[t] for t in type_combo]\n        shapes_str = \", \".join(f\"{s}:{c}\" for s, c in info[\"shapes\"].items())\n        print(f\"  {' . '.join(labels):<35s} -> {info['total']:>4d} (shapes: {shapes_str})\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nEncodes 13 named -games (3-player RPS, Stag Hunt, Public Goods, etc.) and computes their full invariant signatures (42 family-matrix entries + 24 diagnostic cubics). Outputs `atlas_3x3_results.json`\n\nand `atlas_3x3_table.md`\n\n.\n\n`game_invariants/atlas_3x3.py`\n\n:\n\n``` python\nimport json\nfrom itertools import product as iproduct\nimport numpy as np\n\nfrom contrast_blocks_3x3 import all_family_matrices, mean_zero_payoff\nfrom generators_3x3_strategy_only import diagnostic_degree3\n\n# ---------------------------------------------------------------------------\n# Named (3,3) games\n# ---------------------------------------------------------------------------\n\ndef _zeros():\n    return np.zeros((3, 3, 3, 3), dtype=float)\n\ndef rps_3player():\n    u = _zeros()\n    for p in range(3):\n        for s1 in range(3):\n            for s2 in range(3):\n                for s3 in range(3):\n                    profile = (s1, s2, s3)\n                    my_s = profile[p]\n                    payoff = 0\n                    for q in range(3):\n                        if q == p:\n                            continue\n                        opp_s = profile[q]\n                        diff = (my_s - opp_s) % 3\n                        if diff == 1:\n                            payoff += 1  # my_s beats opp_s\n                        elif diff == 2:\n                            payoff -= 1  # opp_s beats my_s\n                    u[p, s1, s2, s3] = payoff\n    return u\n\ndef stag_hunt_3player():\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        my_s = profile[p]\n        if my_s == 1:\n            u[p, s1, s2, s3] = 2.0\n        elif my_s == 0:\n            u[p, s1, s2, s3] = 4.0 if profile == (0, 0, 0) else 0.0\n        else:  # my_s == 2\n            u[p, s1, s2, s3] = 5.0 if profile == (2, 2, 2) else 0.0\n    return u\n\ndef public_goods_3player():\n    r = 1.5  # return multiplier\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        total = sum(profile)\n        my_cost = profile[p]\n        u[p, s1, s2, s3] = -my_cost + r * total / 3.0\n    return u\n\ndef pure_coordination_3player():\n    u = _zeros()\n    for s in range(3):\n        for p in range(3):\n            u[p, s, s, s] = 1.0\n    return u\n\ndef volunteers_dilemma_3player():\n    c, b = 1.0, 3.0\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        my_s = profile[p]\n        someone_volunteered = any(profile[q] == 0 for q in range(3))\n        payoff = (b if someone_volunteered else 0.0)\n        if my_s == 0:\n            payoff -= c\n        u[p, s1, s2, s3] = payoff\n    return u\n\ndef battle_of_sexes_3player():\n    u = _zeros()\n    for s in range(3):\n        for p in range(3):\n            u[p, s, s, s] = 2.0 if p == s else 1.0\n    return u\n\ndef commons_3player():\n    cap = 2\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        total = s1 + s2 + s3\n        if total <= cap:\n            u[p, s1, s2, s3] = float((s1, s2, s3)[p])\n        else:\n            u[p, s1, s2, s3] = 0.0\n    return u\n\ndef majority_3player():\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        counts = [profile.count(s) for s in range(3)]\n        my_count = counts[profile[p]]\n        max_count = max(counts)\n        if counts.count(max_count) > 1:\n            # tied: e.g., all distinct (1,1,1) or pair (2,1,0)... actually\n            # if one strategy has 2 and another 1, max is unique. Tied only\n            # when all 3 strategies appear once each (counts = (1,1,1)).\n            u[p, s1, s2, s3] = 0.0\n        else:\n            u[p, s1, s2, s3] = 1.0 if my_count == max_count else -1.0\n    return u\n\ndef symmetric_anticoord_3player():\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        distinct = len(set(profile))\n        if distinct == 3:\n            u[p, s1, s2, s3] = 1.0\n        elif distinct == 1:\n            u[p, s1, s2, s3] = -1.0\n        else:\n            u[p, s1, s2, s3] = 0.0\n    return u\n\ndef dictator_3player():\n    base = np.array([\n        [3, 1, 0],   # if player 0 plays 0: payoffs (3, 1, 0)\n        [0, 3, 1],   # if player 0 plays 1: payoffs (0, 3, 1)\n        [1, 0, 3],   # if player 0 plays 2: payoffs (1, 0, 3)\n    ], dtype=float)\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        u[p, s1, s2, s3] = base[s1, p]  # s1 = player 0's strategy\n    return u\n\ndef chicken_3player():\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        my_s = profile[p]\n        stayers = [q for q in range(3) if profile[q] != 0]\n        n_stay = len(stayers)\n        if my_s == 0:  # swerve\n            u[p, s1, s2, s3] = 0.0 if n_stay > 0 else 1.0\n        else:  # stay\n            if n_stay == 1:\n                u[p, s1, s2, s3] = 4.0  # sole stayer wins\n            else:\n                u[p, s1, s2, s3] = -8.0  # multiple stayers collide\n    return u\n\ndef matching_pennies_3player():\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        profile = (s1, s2, s3)\n        if p == 0:\n            u[p, s1, s2, s3] = 1.0 if profile[0] == profile[1] else -1.0\n        elif p == 1:\n            u[p, s1, s2, s3] = 1.0 if profile[1] == profile[2] else -1.0\n        else:  # p == 2\n            u[p, s1, s2, s3] = -1.0 if profile[2] == profile[0] else 1.0\n    return u\n\ndef common_interest_3player():\n    Phi = np.zeros((3, 3, 3))\n    for s1, s2, s3 in iproduct(range(3), repeat=3):\n        # Reward distinct-strategy profiles more\n        distinct = len({s1, s2, s3})\n        Phi[s1, s2, s3] = float(distinct + (s1 + s2 + s3) * 0.5)\n    u = _zeros()\n    for p, s1, s2, s3 in iproduct(range(3), repeat=4):\n        u[p, s1, s2, s3] = Phi[s1, s2, s3]\n    return u\n\n# Registry of named games\nNAMED_GAMES = {\n    \"3p_Rock_Paper_Scissors\": rps_3player,\n    \"3p_Stag_Hunt\": stag_hunt_3player,\n    \"3p_Public_Goods\": public_goods_3player,\n    \"3p_Pure_Coordination\": pure_coordination_3player,\n    \"3p_Volunteers_Dilemma\": volunteers_dilemma_3player,\n    \"3p_Battle_of_Sexes\": battle_of_sexes_3player,\n    \"3p_Tragedy_of_Commons\": commons_3player,\n    \"3p_Majority\": majority_3player,\n    \"3p_Symmetric_AntiCoord\": symmetric_anticoord_3player,\n    \"3p_Asymmetric_Dictator\": dictator_3player,\n    \"3p_Chicken\": chicken_3player,\n    \"3p_Matching_Pennies\": matching_pennies_3player,\n    \"3p_Common_Interest\": common_interest_3player,\n}\n\n# ---------------------------------------------------------------------------\n# Evaluation\n# ---------------------------------------------------------------------------\n\ndef _type_label(S):\n    \"\"\"Label a contrast type tuple (axis indices) by a 1-based subset string.\"\"\"\n    return \"{\" + \",\".join(str(i + 1) for i in S) + \"}\"\n\ndef evaluate_game(u):\n    record = {}\n\n    # Family matrices\n    fmats = all_family_matrices(u)\n    family = {}\n    for S, M in fmats.items():\n        label = _type_label(S)\n        family[label] = {\n            \"matrix\": M.tolist(),\n            \"trace\": float(np.trace(M)),\n            \"det\": float(np.linalg.det(M)),\n            \"rank_numerical\": int(np.linalg.matrix_rank(M, tol=1e-10)),\n            \"diagonal\": np.diag(M).tolist(),\n            \"offdiag_sum\": float(np.sum(M) - np.trace(M)),\n            \"offdiag_min\": float(np.min(M[~np.eye(3, dtype=bool)])),\n            \"offdiag_max\": float(np.max(M[~np.eye(3, dtype=bool)])),\n        }\n    record[\"family_matrices\"] = family\n\n    # Diagnostic degree-3 invariants\n    record[\"degree3_diagnostics\"] = diagnostic_degree3(u)\n\n    # Coordination-type / potential-type / harmonic-type quick flags from M_{1,2,3}\n    M3 = fmats[(0, 1, 2)]\n    offdiag_3 = M3[~np.eye(3, dtype=bool)]\n    record[\"flags\"] = {\n        \"M_three_way_rank\": int(np.linalg.matrix_rank(M3, tol=1e-10)),\n        \"M_three_way_all_positive_offdiag\": bool(np.all(offdiag_3 > 1e-10)),\n        \"M_three_way_all_negative_offdiag\": bool(np.all(offdiag_3 < -1e-10)),\n        \"M_three_way_trace\": float(np.trace(M3)),\n        \"M_three_way_det\": float(np.linalg.det(M3)),\n        \"potential_candidate\": bool(\n            np.linalg.matrix_rank(M3, tol=1e-10) <= 1 and np.trace(M3) > 1e-10\n        ),\n        \"coordination_type\": bool(np.all(offdiag_3 > 1e-10)),\n    }\n\n    return record\n\ndef _format_payoff_tensor_compact(u):\n    \"\"\"Compact payoff tensor representation: list of (s1,s2,s3) -> (u0,u1,u2).\"\"\"\n    rows = []\n    for s1, s2, s3 in iproduct(range(3), repeat=3):\n        rows.append({\n            \"profile\": [s1, s2, s3],\n            \"payoffs\": [float(u[p, s1, s2, s3]) for p in range(3)],\n        })\n    return rows\n\ndef build_atlas():\n    \"\"\"Evaluate all named games and assemble the atlas record.\"\"\"\n    atlas = {}\n    for name, builder in NAMED_GAMES.items():\n        print(f\"  evaluating {name} ...\")\n        u = builder()\n        record = evaluate_game(u)\n        record[\"payoff_tensor_compact\"] = _format_payoff_tensor_compact(u)\n        atlas[name] = record\n    return atlas\n\ndef write_json(atlas, path):\n    \"\"\"Write the full atlas as JSON.\"\"\"\n    with open(path, \"w\") as f:\n        json.dump(atlas, f, indent=2)\n\ndef write_markdown_table(atlas, path):\n    \"\"\"Write a condensed markdown table for the paper.\"\"\"\n    header = [\n        \"Game\",\n        \"rank $M_{1,2,3}$\",\n        \"tr $M_{1,2,3}$\",\n        \"off-diag $M_{1,2,3}$\",\n        \"Flag\",\n        \"tr $M_{\\\\{1\\\\}}$\",\n        \"tr $M_{\\\\{1,2\\\\}}$\",\n    ]\n    rows = []\n    for name, rec in atlas.items():\n        flag_parts = []\n        if rec[\"flags\"][\"coordination_type\"]:\n            flag_parts.append(\"coord\")\n        if rec[\"flags\"][\"M_three_way_all_negative_offdiag\"]:\n            flag_parts.append(\"anti-coord\")\n        if rec[\"flags\"][\"potential_candidate\"]:\n            flag_parts.append(\"potential?\")\n        if not flag_parts:\n            flag_parts.append(\"mixed\")\n        flag = \", \".join(flag_parts)\n\n        M3 = rec[\"family_matrices\"][\"{1,2,3}\"]\n        rank3 = M3[\"rank_numerical\"]\n        tr3 = M3[\"trace\"]\n        offdiag_summary = (\n            f\"min={M3['offdiag_min']:+.2f}, max={M3['offdiag_max']:+.2f}\"\n        )\n        trM1 = rec[\"family_matrices\"][\"{1}\"][\"trace\"]\n        trM12 = rec[\"family_matrices\"][\"{1,2}\"][\"trace\"]\n\n        rows.append([\n            name.replace(\"3p_\", \"\").replace(\"_\", \" \"),\n            str(rank3),\n            f\"{tr3:+.3f}\",\n            offdiag_summary,\n            flag,\n            f\"{trM1:+.3f}\",\n            f\"{trM12:+.3f}\",\n        ])\n\n    # Build markdown\n    md_lines = [\"# Atlas of (3,3)-game invariant signatures\",\n                \"\",\n                f\"Generated by `atlas_3x3.py`. {len(atlas)} named games.\",\n                \"\",\n                \"| \" + \" | \".join(header) + \" |\",\n                \"|\" + \"|\".join([\"---\"] * len(header)) + \"|\"]\n    for row in rows:\n        md_lines.append(\"| \" + \" | \".join(row) + \" |\")\n    md_lines.append(\"\")\n    md_lines.append(\"Notes:\")\n    md_lines.append(\"- rank $M_S$ = numerical rank of the 3x3 family matrix for\")\n    md_lines.append(\"  contrast type $S$ (3 = full, 1 = potential candidate, etc.).\")\n    md_lines.append(\"- Flag: coordination-type = all $M_{1,2,3}$ off-diagonals positive;\")\n    md_lines.append(\"  anti-coord = all negative; potential? = rank-1 $M_{1,2,3}$.\")\n    md_lines.append(\"- Full 42 degree-2 entries and 24 degree-3 diagnostics in \"\n                    \"`atlas_3x3_results.json`.\")\n\n    with open(path, \"w\") as f:\n        f.write(\"\\n\".join(md_lines))\n\ndef main():\n    print(f\"Building atlas of {len(NAMED_GAMES)} named (3,3)-games ...\")\n    atlas = build_atlas()\n\n    json_path = \"atlas_3x3_results.json\"\n    md_path = \"atlas_3x3_table.md\"\n\n    write_json(atlas, json_path)\n    write_markdown_table(atlas, md_path)\n\n    print(f\"\\nWrote: {json_path}\")\n    print(f\"Wrote: {md_path}\")\n\n    # Quick summary\n    print(\"\\nSummary of M_{1,2,3} structure per game:\")\n    print(f\"{'Game':<35s} {'rank':>5s} {'trace':>10s} {'off-diag flag':>20s}\")\n    for name, rec in atlas.items():\n        M3 = rec[\"family_matrices\"][\"{1,2,3}\"]\n        rank3 = M3[\"rank_numerical\"]\n        tr3 = M3[\"trace\"]\n        flags = rec[\"flags\"]\n        flag = (\"coord\" if flags[\"coordination_type\"]\n                else \"anti-coord\" if flags[\"M_three_way_all_negative_offdiag\"]\n                else \"potential?\" if flags[\"potential_candidate\"]\n                else \"mixed\")\n        print(f\"{name:<35s} {rank3:>5d} {tr3:>+10.3f} {flag:>20s}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nThree-layer classifier: Layer 1 = 7-bit family-activation pattern, Layer 2 = per-family (rank, off-diag sign), Layer 3 = full invariant fingerprint. Greedy backward elimination finds minimal classifying feature sets for atlas games.\n\n`game_invariants/classify_3x3.py`\n\n:\n\n``` python\nimport json\nfrom itertools import combinations\nimport numpy as np\n\nTOL = 1e-9\n\ndef sign3(x):\n    \"\"\"Three-valued sign: -1, 0, +1.\"\"\"\n    if x > TOL:\n        return 1\n    if x < -TOL:\n        return -1\n    return 0\n\ndef build_features(atlas):\n    family_keys = [\"{1}\", \"{2}\", \"{3}\", \"{1,2}\", \"{1,3}\", \"{2,3}\", \"{1,2,3}\"]\n    features = {}\n\n    for name, rec in atlas.items():\n        f = {}\n        for S in family_keys:\n            fm = rec[\"family_matrices\"][S]\n            M = np.array(fm[\"matrix\"])\n            tr = fm[\"trace\"]\n            rk = fm[\"rank_numerical\"]\n            offdiag_vals = M[~np.eye(3, dtype=bool)]\n            offdiag_signs = set(sign3(v) for v in offdiag_vals)\n            if 0 in offdiag_signs and len(offdiag_signs) > 1:\n                offdiag_signs.discard(0)\n            offdiag_sign_summary = (\n                2 if len(offdiag_signs) > 1\n                else next(iter(offdiag_signs)) if offdiag_signs\n                else 0\n            )\n\n            f[f\"layer1_{S}\"] = int(rk > 0 or abs(tr) > TOL)\n            f[f\"rank_{S}\"] = rk\n            f[f\"trace_sign_{S}\"] = sign3(tr)\n            f[f\"offdiag_sign_{S}\"] = offdiag_sign_summary\n            f[f\"det_sign_{S}\"] = sign3(fm[\"det\"])\n\n        # Cross-family trace comparisons (a few useful ones)\n        traces = {S: rec[\"family_matrices\"][S][\"trace\"] for S in family_keys}\n        f[\"cmp_main_vs_pair\"] = sign3(\n            traces[\"{1}\"] + traces[\"{2}\"] + traces[\"{3}\"]\n            - traces[\"{1,2}\"] - traces[\"{1,3}\"] - traces[\"{2,3}\"]\n        )\n        f[\"cmp_pair_vs_three\"] = sign3(\n            traces[\"{1,2}\"] + traces[\"{1,3}\"] + traces[\"{2,3}\"]\n            - 3 * traces[\"{1,2,3}\"]\n        )\n\n        # Degree-3 sign vector\n        for dname, dval in rec[\"degree3_diagnostics\"].items():\n            f[f\"d3_{dname}_sign\"] = sign3(dval)\n\n        features[name] = f\n\n    return features\n\ndef feature_matrix(features, feature_keys):\n    \"\"\"Build a 2D array of feature values: rows = games, cols = features.\"\"\"\n    games = list(features.keys())\n    mat = np.array([[features[g][k] for k in feature_keys] for g in games])\n    return games, mat\n\ndef patterns_distinct(games, mat):\n    \"\"\"Return True if every pair of games has a distinct feature row.\"\"\"\n    seen = {}\n    for i, g in enumerate(games):\n        row = tuple(mat[i])\n        if row in seen:\n            return False, (seen[row], g)\n        seen[row] = g\n    return True, None\n\ndef minimal_classifying_subset(features, feature_keys, verbose=False):\n    \"\"\"Greedy backward elimination: drop features that aren't needed.\"\"\"\n    games, mat = feature_matrix(features, feature_keys)\n    keep = list(range(len(feature_keys)))\n\n    distinct, collision = patterns_distinct(games, mat[:, keep])\n    if not distinct:\n        if verbose:\n            print(f\"  WARNING: feature set does not separate all games; \"\n                  f\"collision between {collision}\")\n        return [feature_keys[i] for i in keep]\n\n    # Try to drop one feature at a time, lowest-index first\n    changed = True\n    while changed:\n        changed = False\n        for i in list(keep):\n            trial = [j for j in keep if j != i]\n            distinct, _ = patterns_distinct(games, mat[:, trial])\n            if distinct:\n                keep = trial\n                changed = True\n                if verbose:\n                    print(f\"  dropped {feature_keys[i]}; {len(keep)} features remain\")\n                break\n\n    return [feature_keys[i] for i in keep]\n\ndef layer1_signature(atlas):\n    \"\"\"7-bit family-activation pattern per game (which contrast types nonzero).\"\"\"\n    family_keys = [\"{1}\", \"{2}\", \"{3}\", \"{1,2}\", \"{1,3}\", \"{2,3}\", \"{1,2,3}\"]\n    out = {}\n    for name, rec in atlas.items():\n        sig = tuple(\n            int(rec[\"family_matrices\"][S][\"rank_numerical\"] > 0\n                or abs(rec[\"family_matrices\"][S][\"trace\"]) > TOL)\n            for S in family_keys\n        )\n        out[name] = sig\n    return out, family_keys\n\ndef layer2_signature(atlas):\n    \"\"\"Layer-2 signature: (rank, offdiag-sign) per nonzero family.\"\"\"\n    family_keys = [\"{1}\", \"{2}\", \"{3}\", \"{1,2}\", \"{1,3}\", \"{2,3}\", \"{1,2,3}\"]\n    out = {}\n    for name, rec in atlas.items():\n        sig = []\n        for S in family_keys:\n            fm = rec[\"family_matrices\"][S]\n            M = np.array(fm[\"matrix\"])\n            rk = fm[\"rank_numerical\"]\n            if rk == 0 and abs(fm[\"trace\"]) < TOL:\n                sig.append((\"0\", 0))\n                continue\n            offdiag = M[~np.eye(3, dtype=bool)]\n            signs = set(sign3(v) for v in offdiag)\n            if 0 in signs and len(signs) > 1:\n                signs.discard(0)\n            if len(signs) > 1:\n                offdiag_label = \"mixed\"\n            elif signs == {1}:\n                offdiag_label = \"+\"\n            elif signs == {-1}:\n                offdiag_label = \"-\"\n            else:\n                offdiag_label = \"0\"\n            sig.append((str(rk), offdiag_label))\n        out[name] = tuple(sig)\n    return out, family_keys\n\ndef report(atlas_path=\"atlas_3x3_results.json\"):\n    with open(atlas_path) as f:\n        atlas = json.load(f)\n\n    games = list(atlas.keys())\n    print(f\"Classifying invariants for {len(games)} named (3,3)-games\\n\")\n\n    # ------- Layer 1 -------\n    print(\"=\" * 78)\n    print(\"Layer 1: family activation pattern (which of 7 contrast types nonzero)\")\n    print(\"=\" * 78)\n    sigs_l1, fam_keys = layer1_signature(atlas)\n    header = \"Game\".ljust(35) + \" | \" + \"  \".join(s.ljust(7) for s in fam_keys)\n    print(header)\n    print(\"-\" * len(header))\n    pattern_groups = {}\n    for name in games:\n        sig = sigs_l1[name]\n        pattern_groups.setdefault(sig, []).append(name)\n        flags = \"  \".join((\"on\" if b else \"  \").ljust(7) for b in sig)\n        nshort = name.replace(\"3p_\", \"\").replace(\"_\", \" \")\n        print(f\"{nshort:<35s} | {flags}\")\n    print(f\"\\n  Distinct Layer-1 patterns: {len(pattern_groups)}\")\n    for sig, members in sorted(pattern_groups.items()):\n        sig_str = \"\".join(str(b) for b in sig)\n        labels = [m.replace(\"3p_\", \"\").replace(\"_\", \" \") for m in members]\n        print(f\"    pattern {sig_str}: {labels}\")\n\n    # ------- Layer 2 -------\n    print()\n    print(\"=\" * 78)\n    print(\"Layer 2: per-family (rank, off-diag sign)\")\n    print(\"=\" * 78)\n    sigs_l2, _ = layer2_signature(atlas)\n    l2_groups = {}\n    for name in games:\n        l2_groups.setdefault(sigs_l2[name], []).append(name)\n    print(f\"  Distinct Layer-2 signatures: {len(l2_groups)}\")\n    for sig, members in sorted(l2_groups.items()):\n        sig_str = \" \".join(f\"{S}=({r},{o})\" for S, (r, o) in zip(fam_keys, sig))\n        labels = [m.replace(\"3p_\", \"\").replace(\"_\", \" \") for m in members]\n        print(f\"    {sig_str}\")\n        for m in labels:\n            print(f\"      - {m}\")\n    if len(l2_groups) == len(games):\n        print(\"  Layer 2 ALONE distinguishes all 13 games.\")\n    else:\n        n_collisions = sum(1 for v in l2_groups.values() if len(v) > 1)\n        print(f\"  Layer 2 leaves {n_collisions} group(s) unresolved; \"\n              f\"add Layer-3 degree-3 signs to discriminate.\")\n\n    # ------- Minimal classifying subset (greedy) -------\n    print()\n    print(\"=\" * 78)\n    print(\"Minimal classifying subset (greedy backward elimination)\")\n    print(\"=\" * 78)\n    features = build_features(atlas)\n    all_keys = sorted(next(iter(features.values())).keys())\n    # Stable order: layer1 first, then ranks, signs, then degree-3\n    def order_key(k):\n        if k.startswith(\"layer1_\"): return (0, k)\n        if k.startswith(\"rank_\"):   return (1, k)\n        if k.startswith(\"trace_sign_\"):   return (2, k)\n        if k.startswith(\"offdiag_sign_\"): return (3, k)\n        if k.startswith(\"det_sign_\"):     return (4, k)\n        if k.startswith(\"cmp_\"):          return (5, k)\n        if k.startswith(\"d3_\"):           return (6, k)\n        return (7, k)\n    ordered_keys = sorted(all_keys, key=order_key)\n\n    # First check whether the full set distinguishes\n    full_games, full_mat = feature_matrix(features, ordered_keys)\n    distinct_full, collision = patterns_distinct(full_games, full_mat)\n    print(f\"  Full feature set ({len(ordered_keys)} features) \"\n          f\"distinguishes all games: {distinct_full}\")\n    if not distinct_full:\n        print(f\"    Unresolvable collision: {collision}\")\n    else:\n        minimal = minimal_classifying_subset(features, ordered_keys, verbose=False)\n        print(f\"  Minimal classifying subset: {len(minimal)} features\")\n        for k in minimal:\n            print(f\"    - {k}\")\n\n    # Also: which features in the minimal set come from each layer?\n    print()\n    print(\"  Layer breakdown of minimal classifying set:\")\n    layer_counts = {}\n    for k in minimal:\n        layer = order_key(k)[0]\n        layer_counts.setdefault(layer, []).append(k)\n    layer_names = {0: \"L1 activation\", 1: \"L2 rank\", 2: \"L2 trace-sign\",\n                   3: \"L2 offdiag-sign\", 4: \"L2 det-sign\", 5: \"cross-cmp\",\n                   6: \"L3 degree-3\"}\n    for layer in sorted(layer_counts):\n        print(f\"    {layer_names[layer]}: {len(layer_counts[layer])}\")\n        for k in layer_counts[layer]:\n            print(f\"      - {k}\")\n\nif __name__ == \"__main__\":\n    report()\n```\n\nConstructs the 93-feature candidate separating set (42 degree-2 + 51 degree-3), verifies -invariance and generic orbit separation on random samples plus orbit-mates.\n\n`game_invariants/orbit_separation_3x3.py`\n\n:\n\n``` python\nfrom itertools import combinations, combinations_with_replacement\nimport json\nimport numpy as np\n\nfrom contrast_blocks_3x3 import (\n    mean_zero_payoff, project_contrast_block, family_matrix, all_family_matrices\n)\n\ndef _project_block_local(u_p, S):\n    \"\"\"Local representation of T_{S, p} (dimension 2^|S|).\"\"\"\n    full = project_contrast_block(u_p, S)\n    sl = [slice(None)] * 3\n    for axis in range(3):\n        if axis not in S:\n            sl[axis] = 0\n    return full[tuple(sl)]\n\ndef degree2_features(u):\n    \"\"\"42 invariants: upper triangle of M_S for each non-empty S.\"\"\"\n    out = []\n    for r in range(1, 4):\n        for S in combinations((0, 1, 2), r):\n            M = family_matrix(u, S)\n            for i in range(3):\n                for j in range(i, 3):\n                    out.append(M[i, j])\n    return np.array(out)\n\ndef degree3_features_extended(u):\n    u0 = mean_zero_payoff(u)\n    out = []\n\n    # Main-effect polarizations\n    for axis in (0, 1, 2):\n        S = (axis,)\n        Ts = [_project_block_local(u0[p], S) for p in range(3)]\n        for p, q, r in combinations_with_replacement(range(3), 3):\n            out.append(float(np.sum(Ts[p] * Ts[q] * Ts[r])))\n\n    # Pairwise-block determinants\n    for pair in combinations((0, 1, 2), 2):\n        for p in range(3):\n            T = _project_block_local(u0[p], pair)\n            out.append(float(np.linalg.det(T)))\n\n    # Three-way contractions\n    Ds = [_project_block_local(u0[p], (0, 1, 2)) for p in range(3)]\n    for p, q, r in combinations_with_replacement(range(3), 3):\n        out.append(float(np.sum(Ds[p] * Ds[q] * Ds[r])))\n\n    M3 = family_matrix(u, (0, 1, 2))\n    out.append(float(np.trace(M3 @ M3 @ M3)))\n    out.append(float(np.linalg.det(M3)))\n\n    return np.array(out)\n\ndef fingerprint(u):\n    \"\"\"Full fingerprint = degree-2 + extended degree-3 features.\"\"\"\n    return np.concatenate([degree2_features(u), degree3_features_extended(u)])\n\ndef apply_group_element(u, sigma1, sigma2, sigma3):\n    inv1 = np.argsort(sigma1)\n    inv2 = np.argsort(sigma2)\n    inv3 = np.argsort(sigma3)\n    return u[:, inv1][:, :, inv2][:, :, :, inv3]\n\n# ---------------------------------------------------------------------------\n# Tests\n# ---------------------------------------------------------------------------\n\ndef test_orbit_invariance(N=30, tol=1e-8):\n    \"\"\"For N random games, verify the fingerprint is (S_3)^3-invariant.\"\"\"\n    rng = np.random.default_rng(0)\n    max_err = 0.0\n    for trial in range(N):\n        u = rng.standard_normal((3, 3, 3, 3))\n        fp_u = fingerprint(u)\n        # Random group element\n        sigma1 = rng.permutation(3)\n        sigma2 = rng.permutation(3)\n        sigma3 = rng.permutation(3)\n        u_rel = apply_group_element(u, sigma1, sigma2, sigma3)\n        fp_rel = fingerprint(u_rel)\n        err = float(np.max(np.abs(fp_u - fp_rel)))\n        max_err = max(max_err, err)\n    return max_err\n\ndef test_separation_random(N=500, tol=1e-6):\n    rng = np.random.default_rng(1)\n    fps = []\n    labels = []\n    for i in range(N):\n        u = rng.standard_normal((3, 3, 3, 3))\n        fp_u = fingerprint(u)\n        fps.append(fp_u)\n        labels.append(i)\n        sigma1 = rng.permutation(3)\n        sigma2 = rng.permutation(3)\n        sigma3 = rng.permutation(3)\n        u_rel = apply_group_element(u, sigma1, sigma2, sigma3)\n        fps.append(fingerprint(u_rel))\n        labels.append(i)\n    fps = np.array(fps)\n    labels = np.array(labels)\n    M = len(fps)\n\n    # Compare every pair; record cases where fingerprints match\n    pair_matches = []\n    pair_mismatches_in_same_orbit = []\n    for i in range(M):\n        for j in range(i + 1, M):\n            diff = float(np.max(np.abs(fps[i] - fps[j])))\n            same_orbit = (labels[i] == labels[j])\n            close = diff < tol\n            if close and not same_orbit:\n                pair_matches.append((i, j, diff))\n            if not close and same_orbit:\n                pair_mismatches_in_same_orbit.append((i, j, diff))\n\n    return {\n        \"false_merges\": pair_matches,\n        \"broken_orbit_mates\": pair_mismatches_in_same_orbit,\n        \"n_games\": N,\n        \"n_total\": M,\n    }\n\ndef test_atlas_separation(atlas_path=\"atlas_3x3_results.json\"):\n    # Rebuild atlas fingerprints from scratch (the JSON has limited diagnostics)\n    from atlas_3x3 import NAMED_GAMES\n    fps = {}\n    for name, builder in NAMED_GAMES.items():\n        u = builder()\n        fps[name] = fingerprint(u)\n    games = list(fps.keys())\n    n = len(games)\n    collisions = []\n    for i in range(n):\n        for j in range(i + 1, n):\n            diff = float(np.max(np.abs(fps[games[i]] - fps[games[j]])))\n            if diff < 1e-6:\n                collisions.append((games[i], games[j], diff))\n    return collisions, fps\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef _structured_orbit_samples(N_random=80):\n    from atlas_3x3 import NAMED_GAMES\n    samples = []\n    rng = np.random.default_rng(7)\n\n    for _ in range(N_random):\n        samples.append(rng.standard_normal((3, 3, 3, 3)))\n\n    for builder in NAMED_GAMES.values():\n        u = builder()\n        samples.append(u)\n        # Perturbations of atlas games\n        for eps in (0.01, 0.1):\n            samples.append(u + eps * rng.standard_normal((3, 3, 3, 3)))\n\n    # Sparse / structured games\n    for _ in range(20):\n        u = rng.standard_normal((3, 3, 3, 3))\n        mask = rng.random((3, 3, 3, 3)) < 0.3\n        samples.append(u * mask)\n\n    return samples\n\ndef minimum_separating_subset(N=200, tol=1e-6, verbose=False):\n    rng = np.random.default_rng(123)\n    base_samples = _structured_orbit_samples(N_random=N)\n    fps_full = []\n    labels = []\n    for i, u in enumerate(base_samples):\n        fps_full.append(fingerprint(u))\n        labels.append(i)\n        # Add the orbit mate\n        sigma1 = rng.permutation(3)\n        sigma2 = rng.permutation(3)\n        sigma3 = rng.permutation(3)\n        u_rel = apply_group_element(u, sigma1, sigma2, sigma3)\n        fps_full.append(fingerprint(u_rel))\n        labels.append(i)\n    fps_full = np.array(fps_full)\n    labels = np.array(labels)\n\n    n_features = fps_full.shape[1]\n    keep = set(range(n_features))\n\n    def separates(idxs):\n        if not idxs:\n            return False\n        sub = fps_full[:, sorted(idxs)]\n        # Vectorized pairwise check: max-norm distances\n        # For each pair (i, j) with i < j: dist = max(|sub[i] - sub[j]|)\n        # Find the minimum cross-orbit distance.\n        n = sub.shape[0]\n        for i in range(n - 1):\n            diffs = np.max(np.abs(sub[i + 1:] - sub[i]), axis=1)\n            # Mask pairs in same orbit\n            same_orbit = (labels[i + 1:] == labels[i])\n            cross_diffs = diffs[~same_orbit]\n            if cross_diffs.size > 0 and cross_diffs.min() < tol:\n                return False\n        return True\n\n    if not separates(keep):\n        return None  # already cannot separate; shouldn't happen with our 93\n\n    changed = True\n    while changed:\n        changed = False\n        # Try dropping features in order\n        for i in sorted(keep):\n            trial = keep - {i}\n            if separates(trial):\n                keep = trial\n                changed = True\n                if verbose:\n                    print(f\"    dropped feature {i}; {len(keep)} remain\", flush=True)\n                break\n    return sorted(keep)\n\ndef main():\n    print(\"Orbit-separation tests for (3,3)-games\")\n    print(\"=\" * 70)\n\n    # Sample fingerprint size\n    rng = np.random.default_rng(99)\n    u_sample = rng.standard_normal((3, 3, 3, 3))\n    fp_sample = fingerprint(u_sample)\n    print(f\"Fingerprint size: {fp_sample.shape[0]} invariants\")\n    print(f\"  ({degree2_features(u_sample).shape[0]} degree-2 + \"\n          f\"{degree3_features_extended(u_sample).shape[0]} degree-3)\")\n    print()\n\n    # Test A: orbit invariance\n    print(\"(A) Orbit invariance: applying random g in (S_3)^3 to random games ...\")\n    max_err = test_orbit_invariance(N=30)\n    print(f\"  max |fingerprint(u) - fingerprint(g.u)| over 30 trials: {max_err:.2e}\")\n    assert max_err < 1e-8, \"fingerprint is NOT G-invariant\"\n    print(\"  PASS: fingerprint is (S_3)^3-invariant.\")\n    print()\n\n    # Test B: generic separation\n    print(\"(B) Generic orbit separation on N=500 random games + orbit-mates ...\")\n    result = test_separation_random(N=500, tol=1e-6)\n    n_false = len(result[\"false_merges\"])\n    n_broken = len(result[\"broken_orbit_mates\"])\n    print(f\"  pairs in different orbits with same fingerprint (FALSE MERGES): {n_false}\")\n    print(f\"  pairs in same orbit with different fingerprint (BROKEN INVARIANCE): {n_broken}\")\n    assert n_broken == 0\n    if n_false == 0:\n        print(\"  PASS: no false merges. Fingerprint generically separates orbits.\")\n    else:\n        print(f\"  FAIL: {n_false} false merges. Examples:\")\n        for i, j, d in result[\"false_merges\"][:3]:\n            print(f\"    games {i} and {j} differ by {d:.2e} but in different orbits\")\n    print()\n\n    # Test C: atlas separation\n    print(\"(C) Atlas separation: all 13 named games have distinct fingerprints?\")\n    collisions, fps = test_atlas_separation()\n    if collisions:\n        print(f\"  COLLISIONS: {len(collisions)} pairs\")\n        for a, b, d in collisions:\n            print(f\"    {a} <-> {b}: max diff {d:.2e}\")\n    else:\n        print(\"  PASS: all 13 atlas games have distinct fingerprints.\")\n    print()\n\n    # Test D: empirical minimum separating subset against a STRESS-TEST sample.\n    # Note: greedy elimination on any finite sample will return very few features\n    # since any non-constant invariant has distinct values on N generic points.\n    # This is NOT a meaningful lower bound for the full orbit-separation task.\n    # The TRUE minimum separating set for V^0/G has size constrained algebraically\n    # by the geometry of the quotient; for our generating ring we need 42 + 556 = 598\n    # invariants (degrees 2 + 3) minimum, possibly more at higher degrees.\n    print(\"(D) Greedy minimum on stress-test sample ...\")\n    print(\"    (random + atlas + perturbations + sparse, with orbit-mates)\")\n    kept = minimum_separating_subset(N=80, tol=1e-4, verbose=False)\n    print(f\"  Greedy result on this sample: {len(kept)} features suffice.\")\n    print(f\"  CAVEAT: this is sample-specific, not a true separation lower bound.\")\n    print(f\"  For full orbit separation on V^0/G, the generating set (42 deg-2 +\")\n    print(f\"  556 deg-3 = 598 invariants total) is the right target.\")\n    print()\n\n    print(\"=\" * 70)\n    print(\"Summary:\")\n    print(f\"  Fingerprint = {fp_sample.shape[0]} invariants \"\n          f\"({degree2_features(u_sample).shape[0]} deg-2 + \"\n          f\"{degree3_features_extended(u_sample).shape[0]} deg-3)\")\n    print(f\"  Orbit-invariant: max err {max_err:.2e}\")\n    print(f\"  False merges on random sample (N=500, 1000 games total): {n_false}\")\n    print(f\"  Atlas separation: {len(collisions)} collisions\")\n    print(f\"  Greedy minimum separating subset: {len(kept)} features\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nVerifies the candidate separating set on 5 special strata (random, symmetric, low-rank, sparse, near-degenerate). Compares the subalgebra Hilbert series against the full Molien series at low degrees and reports the algebra-coverage gap.\n\n`game_invariants/hilbert_separation_3x3.py`\n\n:\n\n``` python\nfrom itertools import combinations_with_replacement\nimport json\nimport numpy as np\n\nfrom orbit_separation_3x3 import (\n    fingerprint,\n    degree2_features,\n    degree3_features_extended,\n    apply_group_element,\n)\nfrom molien_3x3_strategy_only import molien_33_strategy_only\n\n# ---------------------------------------------------------------------------\n# (1) Subalgebra Hilbert series via numerical rank\n# ---------------------------------------------------------------------------\n\ndef compute_subalgebra_hilbert(max_degree=6, N_points=800, tol=1e-6, verbose=True):\n    # Each f_i has its own degree (2 for the 42 family-matrix entries, 3 for\n    # the 51 degree-3 cubics).\n    fp_degrees = [2] * 42 + [3] * 51\n    n_inv = len(fp_degrees)\n\n    if verbose:\n        print(f\"Evaluating fingerprint at {N_points} random V^0 points ...\",\n              flush=True)\n    rng = np.random.default_rng(11)\n    sample_pts = [rng.standard_normal((3, 3, 3, 3)) for _ in range(N_points)]\n    F_eval = np.array([fingerprint(u) for u in sample_pts])  # (N_points, 93)\n\n    # For each total degree d, enumerate monomials in the 93 invariants\n    # whose total *fingerprint degree* (sum of fp_degrees of chosen invariants)\n    # equals d. Evaluate each monomial as a product across the sample.\n    if verbose:\n        print(f\"Enumerating monomials in subalgebra coordinates up to degree {max_degree} ...\",\n              flush=True)\n    hilb = [0] * (max_degree + 1)\n    hilb[0] = 1  # constants\n\n    # For each abstract polynomial degree d, the contributing monomials are\n    # m_{i1} m_{i2} ... m_{ik} where i_1 <= i_2 <= ... and fp_degrees sum to d.\n    # Enumerate by length k = 1, 2, 3, ... and total degree.\n    for d in range(1, max_degree + 1):\n        # Build all unordered tuples of fingerprint indices whose degree sums to d.\n        mono_evals = []\n        for k in range(1, d // 2 + 1):  # min monomial length is d/d=1, max d/2\n            pass\n        # Simpler: iterate over k (monomial length); for each k, generate\n        # unordered tuples and filter by total degree.\n        # Max k for total degree d: k <= d (when all fp_degrees=1, but our min is 2).\n        # Actually min fp_degree is 2, so max k = d // 2.\n        for k in range(1, d // 2 + 1):\n            for idxs in combinations_with_replacement(range(n_inv), k):\n                if sum(fp_degrees[i] for i in idxs) == d:\n                    val = np.ones(N_points)\n                    for i in idxs:\n                        val *= F_eval[:, i]\n                    mono_evals.append(val)\n        if not mono_evals:\n            hilb[d] = 0\n            if verbose:\n                print(f\"  degree {d}: no monomials -> dim 0\", flush=True)\n            continue\n\n        M = np.array(mono_evals)  # (n_monos, N_points)\n        rank = int(np.linalg.matrix_rank(M, tol=tol))\n        hilb[d] = rank\n        if verbose:\n            print(f\"  degree {d}: {len(mono_evals)} monomials, rank = {rank}\",\n                  flush=True)\n\n    return hilb\n\ndef compare_to_molien(subalgebra_hilbert, max_degree):\n    \"\"\"Compare subalgebra Hilbert series to full Molien series.\"\"\"\n    full = molien_33_strategy_only(max_degree)\n    print()\n    print(\"Hilbert series comparison: subalgebra vs full invariant ring\")\n    print(\"=\" * 70)\n    print(f\"{'degree':>6} {'subalgebra':>12} {'full ring':>12} {'gap':>8}\")\n    print(\"-\" * 70)\n    for d in range(max_degree + 1):\n        gap = full[d] - subalgebra_hilbert[d]\n        flag = \"\" if gap == 0 else \"  <-- gap\" if gap > 0 else \"  ERROR\"\n        print(f\"{d:>6} {subalgebra_hilbert[d]:>12} {full[d]:>12} {gap:>+8}{flag}\")\n    print()\n    total_gap_low = sum(full[d] - subalgebra_hilbert[d] for d in range(min(4, len(full))))\n    return full, total_gap_low\n\n# ---------------------------------------------------------------------------\n# (2) Stress-tests on special strata\n# ---------------------------------------------------------------------------\n\ndef _gen_random(rng):\n    return rng.standard_normal((3, 3, 3, 3))\n\ndef _gen_symmetric_payoff(rng):\n    # Generate via a single random function on (own_strategy, multiset of others)\n    # multiset of 2 strategies from {0,1,2}: 6 possibilities\n    # Total entries: 3 (own) * 6 (multiset) = 18 random numbers\n    base = rng.standard_normal(18)\n    u = np.zeros((3, 3, 3, 3))\n    multiset_index = {}\n    idx = 0\n    for a in range(3):\n        for b in range(a, 3):\n            multiset_index[(a, b)] = idx\n            multiset_index[(b, a)] = idx\n            idx += 1\n    for p in range(3):\n        for s1 in range(3):\n            for s2 in range(3):\n                for s3 in range(3):\n                    profile = [s1, s2, s3]\n                    own = profile[p]\n                    others = sorted(profile[q] for q in range(3) if q != p)\n                    mi = multiset_index[(others[0], others[1])]\n                    u[p, s1, s2, s3] = base[own * 6 + mi]\n    return u\n\ndef _gen_low_rank_payoff(rng, rank=2):\n    u = np.zeros((3, 3, 3, 3))\n    for p in range(3):\n        for _ in range(rank):\n            v1 = rng.standard_normal(3)\n            v2 = rng.standard_normal(3)\n            v3 = rng.standard_normal(3)\n            u[p] += np.einsum('i,j,k->ijk', v1, v2, v3)\n    return u\n\ndef _gen_sparse_payoff(rng, density=0.3):\n    \"\"\"Sparse: zero out (1 - density) fraction of payoff entries.\"\"\"\n    u = rng.standard_normal((3, 3, 3, 3))\n    mask = rng.random((3, 3, 3, 3)) < density\n    return u * mask\n\ndef _gen_near_degenerate(rng, eps=0.05):\n    \"\"\"Near-degenerate: small perturbation of a degenerate (all-zero) tensor.\"\"\"\n    return eps * rng.standard_normal((3, 3, 3, 3))\n\nSTRATA = {\n    \"random\": _gen_random,\n    \"symmetric\": _gen_symmetric_payoff,\n    \"low_rank\": _gen_low_rank_payoff,\n    \"sparse\": _gen_sparse_payoff,\n    \"near_degenerate\": _gen_near_degenerate,\n}\n\ndef stress_test(N_per_stratum=50, tol=1e-6, verbose=True):\n    rng = np.random.default_rng(99)\n    results = {}\n    if verbose:\n        print(\"Stress tests on special strata\")\n        print(\"=\" * 70)\n    for stratum_name, gen in STRATA.items():\n        if verbose:\n            print(f\"  stratum: {stratum_name} ({N_per_stratum} orbits) ...\",\n                  flush=True)\n        fps = []\n        labels = []\n        for i in range(N_per_stratum):\n            u = gen(rng)\n            fps.append(fingerprint(u))\n            labels.append(i)\n            sigma1 = rng.permutation(3)\n            sigma2 = rng.permutation(3)\n            sigma3 = rng.permutation(3)\n            u_rel = apply_group_element(u, sigma1, sigma2, sigma3)\n            fps.append(fingerprint(u_rel))\n            labels.append(i)\n\n        fps = np.array(fps)\n        labels = np.array(labels)\n        n = len(fps)\n\n        false_merges = 0\n        max_in_orbit = 0.0\n        min_cross_orbit = float(\"inf\")\n        for i in range(n - 1):\n            diffs = np.max(np.abs(fps[i + 1:] - fps[i]), axis=1)\n            same_orbit = (labels[i + 1:] == labels[i])\n            in_orbit_diffs = diffs[same_orbit]\n            cross_orbit_diffs = diffs[~same_orbit]\n            if in_orbit_diffs.size > 0:\n                max_in_orbit = max(max_in_orbit, float(in_orbit_diffs.max()))\n            if cross_orbit_diffs.size > 0:\n                min_cross_orbit = min(min_cross_orbit, float(cross_orbit_diffs.min()))\n            false_merges += int((cross_orbit_diffs < tol).sum())\n\n        results[stratum_name] = {\n            \"false_merges\": false_merges,\n            \"max_in_orbit\": max_in_orbit,\n            \"min_cross_orbit\": min_cross_orbit,\n        }\n        if verbose:\n            print(f\"    false merges: {false_merges}\")\n            print(f\"    max in-orbit diff: {max_in_orbit:.2e} (should be ~ machine eps)\")\n            print(f\"    min cross-orbit diff: {min_cross_orbit:.2e}\")\n    return results\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main():\n    print(\"(3a) Stronger separation verification for the 93-invariant fingerprint\")\n    print(\"=\" * 78)\n    print()\n\n    # Hilbert-series check\n    print(\"[1/2] Subalgebra Hilbert-series check (vs full Molien)\")\n    hilb = compute_subalgebra_hilbert(max_degree=4, N_points=400, verbose=True)\n    full, total_gap = compare_to_molien(hilb, max_degree=4)\n    if total_gap == 0:\n        print(\"PASS: subalgebra Hilbert series matches full Molien up to degree 4.\")\n        print(\"Implication: every invariant of degree <= 4 is a polynomial in F.\")\n    else:\n        print(f\"GAP: subalgebra is missing {total_gap} invariants in degrees <= 4.\")\n        print(\"Implication: not all degree-<=4 invariants are polynomials in F.\")\n        print(\"This DOES NOT necessarily mean orbits are unseparated, but it\")\n        print(\"is a warning sign. Adding more degree-3 or degree-4 invariants\")\n        print(\"could close the gap.\")\n    print()\n\n    # Stress-test check\n    print(\"[2/2] Stress-test on special strata\")\n    stress = stress_test(N_per_stratum=50, tol=1e-6)\n    print()\n    print(\"Summary of stress tests:\")\n    print(f\"{'stratum':<20s} {'false_merges':>14s} {'max_in_orbit':>14s} {'min_cross':>14s}\")\n    print(\"-\" * 70)\n    total_merges = 0\n    for stratum, info in stress.items():\n        total_merges += info[\"false_merges\"]\n        print(f\"{stratum:<20s} {info['false_merges']:>14d} \"\n              f\"{info['max_in_orbit']:>14.2e} {info['min_cross_orbit']:>14.2e}\")\n    print()\n    if total_merges == 0:\n        print(\"PASS: no false merges in any stratum. 93 invariants robustly separate.\")\n    else:\n        print(f\"FAIL: {total_merges} false merges. The 93 invariants are not sufficient\")\n        print(\"on every stratum tested.\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nTwo-pass census: Pass 1 enumerates all 128 family-activation patterns and constructs a representative game for each; Pass 2 samples within each cell to enumerate Layer-2 sub-cells. Outputs `typology_census_3x3.json`\n\nand `typology_census_3x3_table.md`\n\n.\n\n`game_invariants/typology_census_3x3.py`\n\n:\n\n```\n\"\"\"Typology census of (3,3)-games under (S_3)^3 (strategy-only relabeling).\"\"\"\n\nfrom collections import Counter\nfrom itertools import combinations, product\nimport json\nimport numpy as np\n\nfrom contrast_blocks_3x3 import (\n    mean_zero_payoff, project_contrast_block, family_matrix, all_family_matrices,\n)\nfrom classify_3x3 import sign3, TOL\n\nTYPES = [(0,), (1,), (2,), (0, 1), (0, 2), (1, 2), (0, 1, 2)]\nTYPE_LABELS = [\"{1}\", \"{2}\", \"{3}\", \"{1,2}\", \"{1,3}\", \"{2,3}\", \"{1,2,3}\"]\n\ndef _pattern_bits(pattern: tuple) -> str:\n    return \"\".join(str(b) for b in pattern)\n\ndef _all_patterns():\n    return list(product([0, 1], repeat=7))\n\ndef construct_game(pattern: tuple, rng: np.random.Generator) -> np.ndarray:\n    u = np.zeros((3, 3, 3, 3))\n    for k, S in enumerate(TYPES):\n        if pattern[k] == 0:\n            continue\n        for p in range(3):\n            raw = rng.standard_normal((3, 3, 3))\n            block = project_contrast_block(raw, S)\n            u[p] += block\n    return u\n\ndef measure_layer1(u: np.ndarray) -> tuple:\n    bits = []\n    for S in TYPES:\n        active = 0\n        for p in range(3):\n            block = project_contrast_block(mean_zero_payoff(u)[p], S)\n            if float(np.max(np.abs(block))) > TOL:\n                active = 1\n                break\n        bits.append(active)\n    return tuple(bits)\n\ndef measure_layer2(u: np.ndarray) -> tuple:\n    fmats = all_family_matrices(u)\n    sig = []\n    for S in TYPES:\n        M = fmats[S]\n        rk = int(np.linalg.matrix_rank(M, tol=1e-10))\n        offdiag = M[~np.eye(3, dtype=bool)]\n        signs = set(sign3(v) for v in offdiag)\n        if 0 in signs and len(signs) > 1:\n            signs.discard(0)\n        if rk == 0 and abs(np.trace(M)) < TOL:\n            label = (\"0\", 0)\n        else:\n            if len(signs) > 1:\n                offlabel = \"M\"\n            elif signs == {1}:\n                offlabel = \"+\"\n            elif signs == {-1}:\n                offlabel = \"-\"\n            else:\n                offlabel = \"0\"\n            label = (str(rk), offlabel)\n        sig.append(label)\n    return tuple(sig)\n\ndef pattern_interpretation(pattern: tuple) -> str:\n    bits = pattern\n    n_main = sum(bits[:3])\n    n_pair = sum(bits[3:6])\n    n_three = bits[6]\n    descriptors = []\n    if n_main == 0 and n_pair == 0 and n_three == 0:\n        return \"trivial (zero game)\"\n    if n_main > 0 and n_pair == 0 and n_three == 0:\n        descriptors.append(\"linear\" if n_main == 3 else f\"linear in {n_main}/3 axes\")\n    if n_main == 0 and n_pair > 0 and n_three == 0:\n        descriptors.append(\"pure pairwise\" if n_pair == 3 else f\"pairwise in {n_pair}/3 pairs\")\n    if n_main == 0 and n_pair == 0 and n_three == 1:\n        descriptors.append(\"pure three-way\")\n    if n_main > 0 and n_pair > 0 and n_three == 0:\n        descriptors.append(\"main + pairwise\")\n    if n_main > 0 and n_pair == 0 and n_three == 1:\n        descriptors.append(\"main + three-way\")\n    if n_main == 0 and n_pair > 0 and n_three == 1:\n        descriptors.append(\"pairwise + three-way\")\n    if n_main > 0 and n_pair > 0 and n_three == 1:\n        descriptors.append(\"full structure\")\n    if not descriptors:\n        descriptors.append(\"mixed\")\n    return \", \".join(descriptors)\n\ndef pass1_layer1_census(rng_seed=0, attempts=8):\n    rng = np.random.default_rng(rng_seed)\n    cells = {}\n    for pattern in _all_patterns():\n        success = False\n        rep_u = None\n        for _ in range(attempts):\n            u = construct_game(pattern, rng)\n            measured = measure_layer1(u)\n            if measured == pattern:\n                success = True\n                rep_u = u\n                break\n        cells[_pattern_bits(pattern)] = {\n            \"pattern\": list(pattern),\n            \"realizable\": success,\n            \"representative\": rep_u.tolist() if rep_u is not None else None,\n            \"n_main\": sum(pattern[:3]),\n            \"n_pair\": sum(pattern[3:6]),\n            \"n_three\": int(pattern[6]),\n            \"interpretation\": pattern_interpretation(pattern),\n        }\n    return cells\n\ndef pass2_layer2_refinement(cells: dict, n_samples=80, rng_seed=1):\n    rng = np.random.default_rng(rng_seed)\n    for key, cell in cells.items():\n        if not cell[\"realizable\"]:\n            cell[\"layer2_subcells\"] = []\n            continue\n        pattern = tuple(cell[\"pattern\"])\n        seen = {}\n        for _ in range(n_samples):\n            u = construct_game(pattern, rng)\n            if measure_layer1(u) != pattern:\n                continue\n            l2 = measure_layer2(u)\n            l2_key = \"|\".join(f\"{a}{b}\" for (a, b) in l2)\n            seen.setdefault(l2_key, {\n                \"signature\": [list(t) for t in l2],\n                \"count\": 0,\n            })\n            seen[l2_key][\"count\"] += 1\n        cell[\"layer2_subcells\"] = [\n            {\"key\": k, \"signature\": v[\"signature\"], \"sample_count\": v[\"count\"]}\n            for k, v in sorted(seen.items(), key=lambda kv: -kv[1][\"count\"])\n        ]\n    return cells\n\ndef place_named_games(cells: dict):\n    from atlas_3x3 import NAMED_GAMES\n    placements = {}\n    for name, builder in NAMED_GAMES.items():\n        u = builder()\n        pattern = measure_layer1(u)\n        l2 = measure_layer2(u)\n        key = _pattern_bits(pattern)\n        placements[name] = {\n            \"layer1_key\": key,\n            \"layer1_pattern\": list(pattern),\n            \"layer2_signature\": [list(t) for t in l2],\n        }\n        if key in cells:\n            cells[key].setdefault(\"named_games\", []).append(name)\n    return placements\n\ndef write_markdown_table(cells: dict, placements: dict, path: str):\n    realizable = [v for v in cells.values() if v[\"realizable\"]]\n    empty = [v for v in cells.values() if not v[\"realizable\"]]\n\n    lines = []\n    lines.append(\"# Typology census of $(3,3)$-games under $(S_3)^3$\")\n    lines.append(\"\")\n    lines.append(f\"- Layer-1 patterns: 128 total\")\n    lines.append(f\"- Realizable: {len(realizable)}\")\n    lines.append(f\"- Empty/unrealizable: {len(empty)}\")\n\n    n_l2 = sum(len(v.get(\"layer2_subcells\", [])) for v in realizable)\n    lines.append(f\"- Total Layer-2 sub-cells (sampled): {n_l2}\")\n    lines.append(f\"- Named-game placements: {sum(len(v.get('named_games', [])) for v in cells.values())}\")\n    lines.append(\"\")\n\n    by_struct = {}\n    for key, cell in cells.items():\n        if not cell[\"realizable\"]:\n            continue\n        struct = cell[\"interpretation\"]\n        by_struct.setdefault(struct, []).append((key, cell))\n\n    lines.append(\"## Layer-1 cells by structural type\")\n    lines.append(\"\")\n    lines.append(\"| Structural type | # cells | # Layer-2 sub-cells | Named games placed |\")\n    lines.append(\"|---|---:|---:|---|\")\n    for struct in sorted(by_struct.keys()):\n        rows = by_struct[struct]\n        n_cells = len(rows)\n        n_l2_struct = sum(len(c.get(\"layer2_subcells\", [])) for _, c in rows)\n        named = []\n        for _, c in rows:\n            named.extend(c.get(\"named_games\", []))\n        named_str = \", \".join(n.replace(\"3p_\", \"\").replace(\"_\", \" \") for n in named) if named else \"—\"\n        lines.append(f\"| {struct} | {n_cells} | {n_l2_struct} | {named_str} |\")\n    lines.append(\"\")\n\n    lines.append(\"## Full Layer-1 census (128 cells)\")\n    lines.append(\"\")\n    lines.append(\"| Pattern | Structural type | Layer-2 sub-cells | Named games |\")\n    lines.append(\"|---|---|---:|---|\")\n    for key in sorted(cells.keys()):\n        cell = cells[key]\n        if not cell[\"realizable\"]:\n            type_str = cell[\"interpretation\"] + \" (empty)\"\n        else:\n            type_str = cell[\"interpretation\"]\n        n_l2 = len(cell.get(\"layer2_subcells\", []))\n        named = cell.get(\"named_games\", [])\n        named_str = \", \".join(n.replace(\"3p_\", \"\").replace(\"_\", \" \") for n in named) if named else \"\"\n        lines.append(f\"| `{key}` | {type_str} | {n_l2} | {named_str} |\")\n    lines.append(\"\")\n\n    lines.append(\"## Pattern bit positions\")\n    lines.append(\"\")\n    lines.append(\"Bit order in the 7-bit signature: ($\\\\{1\\\\}, \\\\{2\\\\}, \\\\{3\\\\}, \\\\{1,2\\\\}, \\\\{1,3\\\\}, \\\\{2,3\\\\}, \\\\{1,2,3\\\\}$).\")\n    lines.append(\"Each bit indicates whether the corresponding contrast family is active in the game.\")\n    lines.append(\"\")\n\n    with open(path, \"w\") as f:\n        f.write(\"\\n\".join(lines))\n\ndef main():\n    print(\"Pass 1: enumerating 128 Layer-1 patterns ...\")\n    cells = pass1_layer1_census(rng_seed=0, attempts=8)\n    n_real = sum(1 for v in cells.values() if v[\"realizable\"])\n    print(f\"  Realizable: {n_real} / 128\")\n    if n_real < 128:\n        for key, v in cells.items():\n            if not v[\"realizable\"]:\n                print(f\"    empty: {key} ({v['interpretation']})\")\n\n    print()\n    print(\"Pass 2: Layer-2 refinement (sampling each realizable cell) ...\")\n    cells = pass2_layer2_refinement(cells, n_samples=80, rng_seed=1)\n    total_l2 = sum(len(v.get(\"layer2_subcells\", [])) for v in cells.values())\n    print(f\"  Total Layer-2 sub-cells: {total_l2}\")\n\n    print()\n    print(\"Placing 13 named games into their Layer-1 cells ...\")\n    placements = place_named_games(cells)\n    for name, info in placements.items():\n        short = name.replace(\"3p_\", \"\").replace(\"_\", \" \")\n        print(f\"  {short:<30s} -> Layer-1 cell {info['layer1_key']}\")\n\n    out_json = \"typology_census_3x3.json\"\n    out_md = \"typology_census_3x3_table.md\"\n\n    serializable = {\n        \"cells\": {\n            k: {kk: vv for kk, vv in v.items() if kk != \"representative\"}\n            for k, v in cells.items()\n        },\n        \"named_game_placements\": placements,\n    }\n    with open(out_json, \"w\") as f:\n        json.dump(serializable, f, indent=2)\n    print(f\"\\nWrote {out_json}\")\n\n    write_markdown_table(cells, placements, out_md)\n    print(f\"Wrote {out_md}\")\n\nif __name__ == \"__main__\":\n    main()\n```\n\nUsed AI to assist drafting, coding, and editing this file. The AI provided suggestions for code structure, function design, and implementation details, which were reviewed and modified by the author as necessary. I’ve checked everything but there’s a reason I haven’t placed this on ArXiv yet. Caveat emptor!\n\n5/17/2026: Initial version\n\n5/18/2026: Added ANOVA-coincidence footnote at the contrast-block decomposition.\n\nEach player assigns a distinct rank to the nine strategy profiles, giving labeled games. Dividing by the relabeling group of order gives .↩︎\n\nThis is sometimes called the “advantage” of the first row over the second row, but we prefer “contrast” as we will generalize these concepts to more than two strategies, where the notion of “advantage” becomes less clear.↩︎\n\nThis is distinct from quotienting by player swaps. The present section uses only the strategy-relabeling group , with players distinguishable. If player swaps are also identified, the acting group is the wreath product , discussed separately in Appendix C.↩︎\n\nIf we are interested in alignment of the two players, we can consider “game harmony” (Zizzo and Tan 2002), defined as (we omit normalization for brevity). This is the cosine similarity of the two players’ payoff perturbations, and we might think of as a measure of “interest alignment” between the players (how much the two players want similar things). However, overall game harmony is not an invariant, as it mixes the sign-flip coordinates in a way that does not satisfy the parity conditions. In contrast, is an invariant that detects a specific type of interaction alignment. Detailed discussion is out of scope of this paper, but the point is that the invariant ring allows us to identify and interpret such conditions in a systematic way.↩︎\n\nThe resulting decomposition into main effects, two-way interactions, and higher-order interactions is identical (in terms of linear algebra) to the ANOVA decomposition of a factorial design. The intuition is not quite the same. Here we need a decomposition of the payoff space that is equivariant under strategy relabeling, and the tensor product of irreducible -representations provides one. The coincidence reflects the fact that both constructions decompose using the same -invariant splitting .↩︎\n\nIt is not strictly necessary to construct an explicit separating subset of size near , but one approach is to compute a SAGBI basis (Robbiano and Sweedler 1990) of the invariant ring and select a subset by Hilbert-series matching against the orbit-quotient algebra (see Derksen and Kemper (2015) and Sturmfels (2008) for the constructions).↩︎", "url": "https://wpnews.pro/news/invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling", "canonical_source": "https://demonstrandom.com/symmetry/posts/cit_for_games/", "published_at": "2026-05-17 04:00:00+00:00", "updated_at": "2026-05-29 16:39:19.483331+00:00", "lang": "en", "topics": ["ai-research"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling", "markdown": "https://wpnews.pro/news/invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling.md", "text": "https://wpnews.pro/news/invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling.txt", "jsonld": "https://wpnews.pro/news/invariant-coordinates-for-normal-form-games-modulo-strategy-relabeling.jsonld"}}