Advent of Code 2020 solutions in Zig
const std = @import("std");

const PATH = "input/day22.txt";
const Str = []const u8;

pub fn first(allocator: ?std.mem.Allocator) anyerror!usize {
    var game = try parseInput(allocator.?, @embedFile(PATH));
    defer {
        game.p1.deinit();
        game.p2.deinit();
    }

    try game.play();

    var sum: usize = 0;
    for (game.p1.items) |item, idx| {
        sum += (game.p1.items.len - idx) * item;
    }
    for (game.p2.items) |item, idx| {
        sum += (game.p2.items.len - idx) * item;
    }

    return sum;
}

pub fn second(allocator: ?std.mem.Allocator) anyerror!usize {
    const game = try parseInput(allocator.?, @embedFile(PATH));
    var rc = RecursiveCombat{
        .p1 = game.p1,
        .p2 = game.p2,
        .p1prev = Cache.init(allocator.?),
        .p2prev = Cache.init(allocator.?),
    };
    defer {
        rc.p1.deinit();
        rc.p2.deinit();
        rc.p1prev.deinit();
        rc.p2prev.deinit();
    }

    _ = try rc.play(allocator.?);

    var sum: usize = 0;
    for (rc.p1.items) |item, idx| {
        sum += (rc.p1.items.len - idx) * item;
    }
    for (rc.p2.items) |item, idx| {
        sum += (rc.p2.items.len - idx) * item;
    }

    return sum;
}

const CardType = u6;
const Game = struct {
    p1: std.ArrayList(CardType),
    p2: std.ArrayList(CardType),

    fn play(self: *@This()) anyerror!void {
        if (self.p1.items.len == 0 or self.p2.items.len == 0) {
            return;
        }

        const p1 = self.p1.orderedRemove(0);
        const p2 = self.p2.orderedRemove(0);

        if (p1 > p2) {
            try self.p1.append(p1);
            try self.p1.append(p2);
        } else {
            try self.p2.append(p2);
            try self.p2.append(p1);
        }

        try self.play();
    }
};

const Cache = std.ArrayList([]CardType);
const RecursiveCombat = struct {
    p1: std.ArrayList(CardType),
    p2: std.ArrayList(CardType),
    p1prev: Cache,
    p2prev: Cache,

    fn play(self: *@This(), allocator: std.mem.Allocator) anyerror!u1 {
        if (self.p2.items.len == 0) {
            return 0;
        }
        if (self.p1.items.len == 0) {
            return 1;
        }

        // Infinite check
        for (self.p1prev.items) |item| {
            if (std.mem.eql(CardType, item, self.p1.items)) return 0;
        }
        for (self.p2prev.items) |item| {
            if (std.mem.eql(CardType, item, self.p2.items)) return 0;
        }

        // Add current decks to previous decks
        const tmp1 = try self.p1.clone();
        const tmp2 = try self.p1.clone();
        defer tmp1.deinit();
        defer tmp2.deinit();
        try self.p1prev.append(tmp1.items);
        try self.p2prev.append(tmp2.items);

        // Start the round with drawing
        const p1 = self.p1.orderedRemove(0);
        const p2 = self.p2.orderedRemove(0);

        var winner: ?u1 = null;

        // Check recursion
        if (p1 <= self.p1.items.len and p2 <= self.p2.items.len) {
            var rc = RecursiveCombat{
                .p1 = try std.ArrayList(CardType).initCapacity(allocator, p1),
                .p2 = try std.ArrayList(CardType).initCapacity(allocator, p2),
                .p1prev = Cache.init(allocator),
                .p2prev = Cache.init(allocator),
            };
            defer {
                rc.p1.deinit();
                rc.p2.deinit();
                rc.p1prev.deinit();
                rc.p2prev.deinit();
            }
            rc.p1.appendSliceAssumeCapacity(self.p1.items[0..p1]);
            rc.p2.appendSliceAssumeCapacity(self.p2.items[0..p2]);

            winner = try rc.play(allocator);
        }

        // Without recursion winner is null, set it
        if (winner == null) {
            if (p1 > p2) winner = 0 else winner = 1;
        }

        if (winner.? == 0) {
            try self.p1.append(p1);
            try self.p1.append(p2);
        } else {
            try self.p2.append(p2);
            try self.p2.append(p1);
        }

        return try self.play(allocator);
    }
};

fn parseInput(allocator: std.mem.Allocator, input: Str) !Game {
    var ret = Game{
        .p1 = std.ArrayList(CardType).init(allocator),
        .p2 = std.ArrayList(CardType).init(allocator),
    };

    var lines = std.mem.split(u8, input, "\n");

    // Player 1
    while (lines.next()) |line| {
        if (line.len == 0) break;
        if (line[0] == 'P') continue;
        const num = try std.fmt.parseUnsigned(CardType, line, 10);
        try ret.p1.append(num);
    }

    // Player 2
    while (lines.next()) |line| {
        if (line.len == 0) break;
        if (line[0] == 'P') continue;
        const num = try std.fmt.parseUnsigned(CardType, line, 10);
        try ret.p2.append(num);
    }

    return ret;
}

test "day22a" {
    try std.testing.expectEqual(@as(usize, 33561), try first(std.testing.allocator));
}

test "day22b" {
    try std.testing.expectEqual(@as(usize, 34594), try second(std.testing.allocator));
}

const test_input =
    \\Player 1:
    \\9
    \\2
    \\6
    \\3
    \\1
    \\
    \\Player 2:
    \\5
    \\8
    \\4
    \\7
    \\10
;

const infinite_test =
    \\Player 1:
    \\43
    \\19
    \\
    \\Player 2:
    \\2
    \\29
    \\14
;