When positions can repeat, the game graph has cycles: plain memoized recursion loops forever, and infinite play (a draw) becomes a possible outcome. Two tools: retrograde analysis (win / loss / draw with optimal game lengths, the way endgame tablebases are built) and the generalized Sprague–Grundy function of Smith and Fraenkel (to combine sums of loopy games).

ToolAnswersCost
retrograde analysisoutcome of every position, moves to the end (winner hurries, loser stalls)
generalized SG a value per position, enough to decide any sum
partizan or “mover matters” gamesrun retrograde on states (position, player to move)doubles the graph

Sum rule for the generalized SG function: all parts finite: XOR rule as usual; exactly one infinite part and XOR of the others: the first player wins iff , otherwise draw; two or more infinite parts: draw.

// adj[v] = positions reachable in one move; a position with no move is a loss for the player to move.
// res[v] = 1 win, -1 loss, 0 draw (for the player to move); len[v] = moves until the end with optimal play.
pair<vector<int>, vector<int>> retrograde(const vector<vector<int>>& adj) {
    int n = sz(adj); vector<vector<int>> radj(n); vector<int> deg(n), res(n, 0), len(n, -1), q;
    for (int v = 0; v < n; v++) {
        deg[v] = sz(adj[v]);
        for (int u : adj[v]) radj[u].push_back(v);
        if (!deg[v]) res[v] = -1, len[v] = 0, q.push_back(v);
    }
    for (int h = 0; h < sz(q); h++) {
        int v = q[h];
        for (int p : radj[v]) {
            if (res[p]) continue;
            if (res[v] == -1) res[p] = 1, len[p] = len[v] + 1, q.push_back(p);           // a losing follower: win
            else if (--deg[p] == 0) res[p] = -1, len[p] = len[v] + 1, q.push_back(p);    // all followers win: loss
        }
    }
    return {res, len};                                       // res 0 = draw
}
 
// Generalized Sprague-Grundy (Smith): g[v] >= 0 finite value, or -1 (infinity) with K[v] = sorted finite follower values.
pair<vector<int>, vector<vector<int>>> loopyGrundy(const vector<vector<int>>& adj) {
    int n = sz(adj); vector<vector<int>> radj(n);
    for (int v = 0; v < n; v++) for (int u : adj[v]) radj[u].push_back(v);
    const int OPEN = -2, INF = -1;
    vector<int> g(n, OPEN), cnt(n), q; vector<char> has(n); int open = n;
    for (int i = 0; open > 0; i++) {
        // has[u]: u has a follower of value i; cnt[u]: followers that are open, or infinite without a follower of value i
        fill(all(has), 0); q.clear();
        for (int u = 0; u < n; u++) {
            cnt[u] = 0;
            for (int v : adj[u]) cnt[u] += g[v] < 0;
            if (g[u] == OPEN && !cnt[u]) q.push_back(u);
        }
        while (!q.empty()) {
            int u = q.back(); q.pop_back();
            if (g[u] != OPEN || has[u] || cnt[u]) continue;
            g[u] = i, open--;
            for (int p : radj[u]) {
                cnt[p]--;
                if (has[p]) continue;
                has[p] = 1;
                if (g[p] >= 0) continue;
                for (int r : radj[p]) if (--cnt[r] == 0 && g[r] == OPEN && !has[r]) q.push_back(r);
            }
        }
        for (int u = 0; u < n; u++) if (g[u] == OPEN && !has[u]) g[u] = INF, open--;
    }
    vector<vector<int>> K(n);
    for (int u = 0; u < n; u++) {
        if (g[u] != INF) continue;
        for (int v : adj[u]) if (g[v] >= 0) K[u].push_back(g[v]);
        sort(all(K[u])); K[u].erase(unique(all(K[u])), K[u].end());
    }
    return {g, K};
}
 
int loopyOutcome(int g, const vector<int>& K) {             // single game: 1 win, -1 loss, 0 draw
    if (g >= 0) return g ? 1 : -1;
    return binary_search(all(K), 0) ? 1 : 0;
}
 
int loopySumOutcome(const vector<pair<int, vector<int>>>& parts) {   // parts = {(g, K)} of the components
    int x = 0, inf = 0; const vector<int>* K = nullptr;
    for (auto& [g, k] : parts) { if (g >= 0) x ^= g; else inf++, K = &k; }
    if (inf == 0) return x ? 1 : -1;
    if (inf >= 2) return 0;
    return binary_search(all(*K), x) ? 1 : 0;
}

Definition used by loopyGrundy (Smith 1966, Fraenkel): iff (1) no follower has value , (2) every is the value of some follower, and (3) every follower of value has itself a follower of value (so the opponent’s escape to an infinite position can be reversed). The values are assigned in rounds ; positions that can never satisfy the conditions get .

Applications. Endgame tablebases (chess, checkers), token games on general graphs, “cops and robbers”-type pursuit games (state = both positions plus the mover), and any contest game where a move can return to an earlier state.