🍯 Glaze

package main

import "core:bytes"
import "core:fmt"
import "core:io"
import "core:os"
import "core:strconv"
import "core:strings"

Op :: enum {
	Add,
	Mul,
}

Monkey :: struct {
	items:          [dynamic]uint,
	new_op:         Op,
	new_arg:        uint,
	new_arg_old:    bool,
	test:           uint,
	if_true_index:  int,
	if_false_index: int,
	num_inspected:  int,
}

// NOTE: Have to free result.
parse_input_file :: proc(filepath: string) -> [dynamic]Monkey {
	data, ok := os.read_entire_file_from_filename(filepath)
	if !ok {
		panic("oh no, could not read file")
	}

	buf: bytes.Buffer
	bytes.buffer_init(&buf, data)
	defer bytes.buffer_destroy(&buf)

	monkeys := make([dynamic]Monkey, 0, 10)

	line: string
	err: io.Error
	for ; err != .EOF; line, err = bytes.buffer_read_string(&buf, '\n') {
		if line == "" || line == "\n" {
			continue
		}
		line = line[:len(line) - 1]

		monkey: Monkey
		monkey.items = make([dynamic]uint, 0, 2)

		line, err = bytes.buffer_read_string(&buf, '\n')
		line = line[:len(line) - 1]
		{
			item_index := strings.last_index_byte(line, ':') + 1
			items_split := strings.split(line[item_index:], ",")
			for id in items_split {
				append(&monkey.items, uint(strconv.atoi(id[1:])))
			}
			delete(items_split)
		}


		line, err = bytes.buffer_read_string(&buf, '\n')
		line = line[:len(line) - 1]
		{
			if strings.last_index_byte(line, '+') > 0 {
				monkey.new_op = .Add
			} else if strings.last_index_byte(line, '*') > 0 {
				monkey.new_op = .Mul
			}
			arg_index := strings.last_index_byte(line, ' ') + 1
			if line[arg_index:] != "old" {
				monkey.new_arg = uint(strconv.atoi(line[arg_index:]))
			} else {
				monkey.new_arg_old = true
			}
		}

		line, err = bytes.buffer_read_string(&buf, '\n')
		line = line[:len(line) - 1]
		{
			index := strings.last_index_byte(line, ' ') + 1
			monkey.test = uint(strconv.atoi(line[index:]))
		}

		line, err = bytes.buffer_read_string(&buf, '\n')
		line = line[:len(line) - 1]
		{
			index := strings.last_index_byte(line, ' ') + 1
			monkey.if_true_index = strconv.atoi(line[index:])
		}

		line, err = bytes.buffer_read_string(&buf, '\n')
		line = line[:len(line) - 1]
		{
			index := strings.last_index_byte(line, ' ') + 1
			monkey.if_false_index = strconv.atoi(line[index:])
		}

		_, err = bytes.buffer_read_string(&buf, '\n')

		append(&monkeys, monkey)
	}

	return monkeys
}

print_monkey_items :: proc(monkeys: []Monkey) {
	for monkey, i in monkeys {
		fmt.printf("Monkey %d: %v\n", i, monkey.items)
	}
}

task1 :: proc(monkeys: []Monkey, debug := false) -> int {
	if debug {
		print_monkey_items(monkeys)
	}

	for round in 1 ..= 20 {
		for monkey, monkey_index in monkeys {
			if debug {
				fmt.printf("Monkey %d:\n", monkey_index)
			}
			for item in monkey.items {
				if debug {
					fmt.printf("\tMonkey inspects an item with a worry level of %d.\n", item)
				}

				worry: uint
				if monkey.new_op == .Add {
					worry = item + (monkey.new_arg if !monkey.new_arg_old else item)
				} else if monkey.new_op == .Mul {
					worry = item * (monkey.new_arg if !monkey.new_arg_old else item)
				}

				if debug {
					fmt.printf(
						"\t\tWorry level is %s by %d to %d.\n",
						monkey.new_op,
						monkey.new_arg if !monkey.new_arg_old else item,
						worry,
					)
				}

				worry /= 3
				if debug {
					fmt.printf(
						"\t\tMonkey gets bored with item. Worry level is divided by 3 to %d.\n",
						worry,
					)
				}

				throw_to: int
				if worry % monkey.test == 0 {
					if debug {
						fmt.printf("\t\tCurrent worry level is divisible by %d.\n", monkey.test)
					}
					throw_to = monkey.if_true_index
				} else {
					if debug {
						fmt.printf(
							"\t\tCurrent worry level is not divisible by %d.\n",
							monkey.test,
						)
					}
					throw_to = monkey.if_false_index
				}
				if debug {
					fmt.printf(
						"\t\tItem with worry level %d is thrown to monkey %d.\n",
						worry,
						throw_to,
					)
				}
				append(&monkeys[throw_to].items, worry)
			}

			monkeys[monkey_index].num_inspected += len(monkeys[monkey_index].items)
			delete(monkeys[monkey_index].items)
			monkeys[monkey_index].items = make([dynamic]uint, 0, 2)
		}

		if debug {
			fmt.printf("\nRound %d\n", round)
			print_monkey_items(monkeys)
		}
	}

	if debug {
		fmt.printf("\n")
	}
	first, second: int
	for monkey, i in monkeys {
		if debug {
			fmt.printf("Monkey %d inspected items %d times\n", i, monkey.num_inspected)
		}
		if monkey.num_inspected > first {
			second, first = first, monkey.num_inspected
		} else if monkey.num_inspected > second {
			second = monkey.num_inspected
		}
	}

	return first * second
}

task2 :: proc(monkeys: []Monkey, debug := false) -> int {
	if debug {
		print_monkey_items(monkeys)
	}

	mod_constant: uint = 1
	for monkey, monkey_index in monkeys {
		mod_constant *= monkey.test
	}

	for round in 1 ..= 10000 {
		for monkey, monkey_index in monkeys {
			if debug {
				fmt.printf("Monkey %d:\n", monkey_index)
			}
			for item in monkey.items {
				if debug {
					fmt.printf("\tMonkey inspects an item with a worry level of %d.\n", item)
				}

				worry: uint
				if monkey.new_op == .Add {
					worry = item + (monkey.new_arg if !monkey.new_arg_old else item)
				} else if monkey.new_op == .Mul {
					worry = item * (monkey.new_arg if !monkey.new_arg_old else item)
				}

				if debug {
					fmt.printf(
						"\t\tWorry level is %s by %d to %d.\n",
						monkey.new_op,
						monkey.new_arg if !monkey.new_arg_old else item,
						worry,
					)
				}

				worry %= mod_constant

				throw_to: int
				if worry % monkey.test == 0 {
					if debug {
						fmt.printf("\t\tCurrent worry level is divisible by %d.\n", monkey.test)
					}
					throw_to = monkey.if_true_index
				} else {
					if debug {
						fmt.printf(
							"\t\tCurrent worry level is not divisible by %d.\n",
							monkey.test,
						)
					}
					throw_to = monkey.if_false_index
				}
				if debug {
					fmt.printf(
						"\t\tItem with worry level %d is thrown to monkey %d.\n",
						worry,
						throw_to,
					)
				}
				append(&monkeys[throw_to].items, worry)
			}

			monkeys[monkey_index].num_inspected += len(monkeys[monkey_index].items)
			delete(monkeys[monkey_index].items)
			monkeys[monkey_index].items = make([dynamic]uint, 0, 2)
		}

		if debug {
			fmt.printf("\nRound %d\n", round)
			print_monkey_items(monkeys)
		}
		if round == 1 || round == 20 || round % 1000 == 0 {
			fmt.printf("\nAfter round %d\n", round)
			for monkey, i in monkeys {
				fmt.printf("Monkey %d inspected items %d times\n", i, monkey.num_inspected)
			}
		}
	}

	if debug {
		fmt.printf("\n")
	}
	first, second: int
	for monkey, i in monkeys {
		if debug {
			fmt.printf("Monkey %d inspected items %d times\n", i, monkey.num_inspected)
		}
		if monkey.num_inspected > first {
			second, first = first, monkey.num_inspected
		} else if monkey.num_inspected > second {
			second = monkey.num_inspected
		}
	}

	return first * second
}

free_monkeys :: proc(monkeys: []Monkey) {
	for monkey in monkeys {
		delete(monkey.items)
	}
}

main :: proc() {
	{
		monkeys := parse_input_file("input.txt")
		defer delete(monkeys)
		defer free_monkeys(monkeys[:])

		result1 := task1(monkeys[:])
		fmt.printf("Task 1 result: %d\n", result1)
	}

	{
		monkeys := parse_input_file("input.txt")
		defer delete(monkeys)
		defer free_monkeys(monkeys[:])

		result2 := task2(monkeys[:])
		fmt.printf("Task 2 result: %d\n", result2)
	}
}