combo_games/cold_games.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <iostream> | ||
| 4 | #include <vector> | ||
| 5 | #include <optional> | ||
| 6 | #include <array> | ||
| 7 | #include <cassert> | ||
| 8 | |||
| 9 | // TODO: Make this generic over numerator type, e.g. bignum | ||
| 10 | struct dyadic { | ||
| 11 | int n = 0, d = 0; | ||
| 12 | ✗ | friend std::ostream& operator << (std::ostream& o, dyadic v) { | |
| 13 | ✗ | return o << v.n << "/2^" << v.d; | |
| 14 | } | ||
| 15 | ✗ | friend auto operator == (dyadic a, dyadic b) { | |
| 16 | ✗ | return a.n == b.n && a.d == b.d; | |
| 17 | } | ||
| 18 | ✗ | friend auto operator <=> (dyadic a, dyadic b) { | |
| 19 | ✗ | return a.n << std::max(b.d - a.d, 0) <=> b.n << std::max(a.d - b.d, 0); | |
| 20 | } | ||
| 21 | ✗ | dyadic operator - () const { | |
| 22 | ✗ | return {-n, d}; | |
| 23 | } | ||
| 24 | ✗ | friend dyadic operator + (dyadic a, dyadic b) { | |
| 25 | ✗ | int nn = (a.n << std::max(b.d - a.d, 0)) + (b.n << std::max(a.d - b.d, 0)); | |
| 26 | ✗ | int nd = std::max(a.d, b.d); | |
| 27 | ✗ | if (!nn) nd = 0; | |
| 28 | else { | ||
| 29 | ✗ | int x = std::min(__builtin_ctz(nn), nd); | |
| 30 | ✗ | nn >>= x; | |
| 31 | ✗ | nd -= x; | |
| 32 | } | ||
| 33 | ✗ | return {nn, nd}; | |
| 34 | } | ||
| 35 | }; | ||
| 36 | |||
| 37 | // TODO: Make this generic over dyadic type | ||
| 38 | struct cold_ish { | ||
| 39 | dyadic v; | ||
| 40 | ✗ | bool star = false; | |
| 41 | ✗ | friend std::ostream& operator << (std::ostream& o, cold_ish d) { | |
| 42 | ✗ | return o << d.v << (d.star ? "*" : ""); | |
| 43 | } | ||
| 44 | |||
| 45 | ✗ | friend bool operator == (cold_ish a, cold_ish b) = default; | |
| 46 | |||
| 47 | ✗ | cold_ish operator - () const { | |
| 48 | ✗ | return {-v, star}; | |
| 49 | } | ||
| 50 | ✗ | friend cold_ish operator + (cold_ish a, cold_ish b) { | |
| 51 | ✗ | return {a.v + b.v, bool(a.star ^ b.star)}; | |
| 52 | } | ||
| 53 | }; | ||
| 54 | |||
| 55 | // If L_options <| a <| R_options and no (direct) ancestors of a | ||
| 56 | // satisfy this (by breaking it on the correct side), then G = a. | ||
| 57 | // | ||
| 58 | // Proof: | ||
| 59 | // L can move to L_options - a <| 0 which means R wins as the next player, | ||
| 60 | // or G - a.R so R moves to some R_options - a.R <= 0 which means R wins as the 2nd player | ||
| 61 | // (this requires that a.R breaks it by being >= some R_options and not <= some L_options, | ||
| 62 | // which holds if everything here is cold-ish). | ||
| 63 | |||
| 64 | // 0 are moves for l | ||
| 65 | // TODO: Could optimize | ||
| 66 | ✗ | inline std::optional<cold_ish> cold_ish_game(std::array<std::vector<cold_ish>, 2> moves) { | |
| 67 | ✗ | std::array<std::optional<cold_ish>, 2> best; | |
| 68 | ✗ | for (int z = 0; z < 2; z++) { | |
| 69 | ✗ | std::optional<cold_ish> v; | |
| 70 | ✗ | for (auto m : moves[z]) { | |
| 71 | ✗ | if (z) m = -m; | |
| 72 | // no star is "bigger" in this sense | ||
| 73 | ✗ | if (!v || m.v > v->v || (m.v == v->v && m.star < v->star)) { | |
| 74 | ✗ | v = m; | |
| 75 | } | ||
| 76 | } | ||
| 77 | ✗ | if (z && v) v = -*v; | |
| 78 | ✗ | best[z] = v; | |
| 79 | } | ||
| 80 | ✗ | auto fuzzy_less = [&](std::optional<cold_ish> a, std::optional<cold_ish> b) -> bool { | |
| 81 | ✗ | if (!a) return true; | |
| 82 | ✗ | if (!b) return true; | |
| 83 | ✗ | return (a->v < b->v) || (a->v == b->v && a->star != b->star); | |
| 84 | }; | ||
| 85 | ✗ | if (best[0] && best[1] && fuzzy_less(*best[1], *best[0])) { | |
| 86 | // invalid | ||
| 87 | ✗ | return std::nullopt; | |
| 88 | } | ||
| 89 | |||
| 90 | // best[0] <= best[1], so we should be able to find someone here | ||
| 91 | |||
| 92 | // the only time we return star is if the lower bound == the upper bound | ||
| 93 | ✗ | if (best[0] && best[1] && *best[0] == *best[1]) { | |
| 94 | ✗ | auto cnd = *best[0]; | |
| 95 | ✗ | cnd.star ^= true; | |
| 96 | ✗ | assert(fuzzy_less(best[0], cnd) && fuzzy_less(cnd, best[1])); | |
| 97 | ✗ | return cnd; | |
| 98 | } | ||
| 99 | ✗ | assert(!best[0] || !best[1] || best[0]->v < best[1]->v); | |
| 100 | |||
| 101 | ✗ | cold_ish zero{{0}, false}; | |
| 102 | ✗ | bool le_zero = fuzzy_less(best[0], zero); | |
| 103 | ✗ | bool ge_zero = fuzzy_less(zero, best[1]); | |
| 104 | ✗ | assert(le_zero || ge_zero); | |
| 105 | ✗ | if (le_zero && ge_zero) { | |
| 106 | ✗ | return zero; | |
| 107 | } | ||
| 108 | ✗ | bool flip = le_zero; | |
| 109 | ✗ | if (flip) { | |
| 110 | ✗ | std::swap(le_zero, ge_zero); | |
| 111 | ✗ | std::swap(best[0], best[1]); | |
| 112 | ✗ | if (best[0]) best[0] = -*best[0]; | |
| 113 | ✗ | if (best[1]) best[1] = -*best[1]; | |
| 114 | } | ||
| 115 | ✗ | assert(ge_zero); | |
| 116 | ✗ | assert(!le_zero); | |
| 117 | // check if there's an integer that's good | ||
| 118 | ✗ | assert(best[0]); | |
| 119 | |||
| 120 | // best[0] >= 0 | ||
| 121 | ✗ | int int_cnd = (best[0]->star ? best[0]->v.n - 1 : best[0]->v.n); | |
| 122 | ✗ | assert(int_cnd >= 0); | |
| 123 | ✗ | int_cnd >>= best[0]->v.d; | |
| 124 | ✗ | int_cnd++; | |
| 125 | ✗ | cold_ish cnd = {dyadic(int_cnd)}; | |
| 126 | ✗ | assert(fuzzy_less(best[0], cnd)); | |
| 127 | ✗ | if (fuzzy_less(cnd, best[1])) { | |
| 128 | ✗ | return flip ? -cnd : cnd; | |
| 129 | } | ||
| 130 | |||
| 131 | // otherwise, cnd is too big, and we're just doing the dyadic rational thing | ||
| 132 | ✗ | assert(best[1]); | |
| 133 | // extra 1 to allow the middle | ||
| 134 | ✗ | int d = std::max(best[0]->v.d, best[1]->v.d)+1; | |
| 135 | // exclusive bounds | ||
| 136 | ✗ | int v0 = (best[0]->v.n << (d - best[0]->v.d)) - best[0]->star; | |
| 137 | ✗ | int v1 = (best[1]->v.n << (d - best[1]->v.d)) + best[1]->star; | |
| 138 | ✗ | assert(v1 - v0 >= 2); | |
| 139 | ✗ | int b = 31 - __builtin_clz((v1-1) ^ v0); | |
| 140 | ✗ | cnd = cold_ish{dyadic((v1-1) >> b, d-b), false}; | |
| 141 | ✗ | assert(cnd.v.n & 1); | |
| 142 | ✗ | assert(fuzzy_less(best[0], cnd) && fuzzy_less(cnd, best[1])); | |
| 143 | ✗ | return flip ? -cnd : cnd; | |
| 144 | } | ||
| 145 |