package main import "core:fmt" import "core:math/rand" import os "core:os/os2" import "core:strconv" import "core:time" import "./console" // TODO: Refactor combat (only use monster array for base values) // TODO: Colors // TODO: TUI // TODO: Refactor message history // TODO: Add randomised varied messages // TODO(console): need major cleanup, better color and drawing support (virtual bitmap), functional non-blocking keyboard input, etc // TODO(console): look over signal management // TODO: Print final statistics on stop/quit // TODO: Save progress /* // Source - https://stackoverflow.com/a // Posted by Lucas S. // Retrieved 2026-01-14, License - CC BY-SA 3.0 int khbit() const { struct timeval tv; fd_set fds; tv.tv_sec = 0; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); return FD_ISSET(STDIN_FILENO, &fds); } void nonblock(int state) const { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); if ( state == 1) { ttystate.c_lflag &= (~ICANON & ~ECHO); //Not display character ttystate.c_cc[VMIN] = 1; } else if (state == 0) { ttystate.c_lflag |= ICANON; } tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } bool keyState(int key) const //Use ASCII table { bool pressed; int i = khbit(); //Alow to read from terminal if (i != 0) { char c = fgetc(stdin); if (c == (char) key) { pressed = true; } else { pressed = false; } } return pressed; } int main() { nonblock(1); int i = 0; while (!i) { if (cmd.keyState(32)) //32 in ASCII table correspond to Space Bar { i = 1; } } nonblock(0); return 0; } */ Segment_Kind :: enum { Empty, Monster, Treasure, } Segment_Data :: union { Empty, Monster, Treasure, } Segment :: struct { data: Segment_Data, } Glyph :: struct { glyph: rune, // color // options } Empty :: struct { using _: Glyph, } Hero :: struct { using _: Glyph, state: Hero_State, level: int, hp: int, max_hp: int, base_atk: int, base_def: int, xp: int, xp_to_level: int, weapon_tier: int, armor_tier: int, weapon_bonus: int, armor_bonus: int, gold: int, } Hero_State :: enum { Walking, Combat, Dead, } Monster :: struct { using _: Glyph, name: string, hp: int, atk: int, def: int, xp_reward: int, weight: int, floor_requirement: int, } Treasure :: struct { using _: Glyph, kind: Treasure_Kind, tier: int, value: int, } Treasure_Kind :: enum { Heal, Gold, Weapon, Armor, } MONSTERS := []Monster { {glyph = 'g', name = "Goblin", hp = 10, atk = 3, def = 0, xp_reward = 5, weight = 30, floor_requirement = 1}, {glyph = 's', name = "Slime", hp = 1, atk = 1, def = 0, xp_reward = 1, weight = 40, floor_requirement = 0}, {glyph = 'o', name = "Orc", hp = 20, atk = 6, def = 2, xp_reward = 12, weight = 5, floor_requirement = 7}, {glyph = 'M', name = "Minotaur", hp = 40, atk = 10, def = 4, xp_reward = 35, weight = 1, floor_requirement = 9}, } TREASURE := []Treasure { {glyph = '♡', kind = .Heal}, {glyph = '$', kind = .Gold}, {glyph = 'W', kind = .Weapon}, {glyph = 'A', kind = .Armor}, } generate_floor :: proc(segments: []Segment, current_floor: int, floor_weights: [Segment_Kind]int) { floor_total_weights: int for w in floor_weights { floor_total_weights += w } monster_total_weights: int for m in MONSTERS { if current_floor >= m.floor_requirement { monster_total_weights += m.weight } } for &segment in segments { segment_kind: Segment_Kind roll := rand.int_max(floor_total_weights) running := 0 for w, i in floor_weights { running += w if roll < running { segment_kind = i break } } switch segment_kind { case .Empty: segment.data = Empty { glyph = '-', } case .Monster: monster: Monster m_roll := rand.int_max(monster_total_weights) m_running := 0 for m in MONSTERS { if current_floor < m.floor_requirement { continue } m_running += m.weight if m_roll < m_running { monster = m break } } monster.hp += current_floor * 2 monster.atk += current_floor if current_floor % 2 == 0 { monster.def += current_floor } segment.data = monster case .Treasure: treasure := TREASURE[rand.int_max(len(TREASURE))] if roll >= 95 { #partial switch treasure.kind { case .Gold: treasure.value += current_floor * 2 case .Weapon: treasure.tier = current_floor / (2 + (rand.int_max(2) - 1)) treasure.value = treasure.tier * 3 case .Armor: treasure.tier = current_floor / (2 + (rand.int_max(2) - 1)) treasure.value = treasure.tier * 2 } } else { #partial switch treasure.kind { case .Gold: treasure.value += current_floor case .Weapon: treasure.tier = current_floor / (3 + (rand.int_max(2) - 1)) treasure.value = treasure.tier * 2 case .Armor: treasure.tier = current_floor / (3 + (rand.int_max(2) - 1)) treasure.value = treasure.tier } } segment.data = treasure } } } main :: proc() { ticks_per_second := 5 if len(os.args) > 1 { for arg, i in os.args { if arg == "-ticks" { if i + 1 >= len(os.args) { continue } ticks := strconv.parse_int(os.args[i + 1]) or_else 5 ticks_per_second = ticks if ticks > 0 else ticks_per_second } } } con := console.create() defer console.destroy(&con) current_floor := 0 floor_weights := [Segment_Kind]int { .Empty = 60, .Monster = 25, .Treasure = 10, } floor_segments := [30]Segment{} generate_floor(floor_segments[:], current_floor, floor_weights) hero := Hero { glyph = '@', state = .Walking, level = 1, hp = 20, max_hp = 20, base_atk = 3, base_def = 1, xp = 0, xp_to_level = 10, weapon_tier = 0, armor_tier = 0, weapon_bonus = 0, armor_bonus = 0, gold = 0, } deaths := 0 hero_position := 0 hero_attacking := true message_history: [128]string message_history_slot: int ticks_ms := time.Duration(1000 / ticks_per_second) * time.Millisecond last_tick := time.now() running := true for running { now := time.now() elapsed := time.diff(last_tick, now) if elapsed > ticks_ms { // LOGIC switch hero.state { case .Walking: hero_position += 1 if hero.hp < hero.max_hp { hero.hp += 1 } if hero_position >= len(floor_segments) { current_floor += 1 hero_position = 0 message_history[message_history_slot] = "Found a stairwell, going deeper..." message_history_slot = (message_history_slot + 1) % len(message_history) generate_floor(floor_segments[:], current_floor, floor_weights) } switch v in floor_segments[hero_position].data { case Empty: case Monster: hero.state = .Combat hero_attacking = true message_history[message_history_slot] = "A monster!" message_history_slot = (message_history_slot + 1) % len(message_history) case Treasure: treasure := &floor_segments[hero_position].data.(Treasure) switch treasure.kind { case .Heal: hero.hp += hero.max_hp / 3 if hero.hp > hero.max_hp { hero.hp = hero.max_hp } message_history[message_history_slot] = "Found a healing potion. Tasty!" message_history_slot = (message_history_slot + 1) % len(message_history) case .Gold: hero.gold += treasure.value message_history[message_history_slot] = "Picked up gold" message_history_slot = (message_history_slot + 1) % len(message_history) case .Weapon: if treasure.tier > hero.weapon_tier { hero.weapon_tier = treasure.tier hero.weapon_bonus = treasure.value message_history[message_history_slot] = "Found a better weapon" message_history_slot = (message_history_slot + 1) % len(message_history) } else { message_history[message_history_slot] = "Found a worse weapon" message_history_slot = (message_history_slot + 1) % len(message_history) } case .Armor: if treasure.tier > hero.armor_tier { hero.armor_tier = treasure.tier hero.armor_bonus = treasure.value message_history[message_history_slot] = "Found a better armor" message_history_slot = (message_history_slot + 1) % len(message_history) } else { message_history[message_history_slot] = "Found a worse armor" message_history_slot = (message_history_slot + 1) % len(message_history) } } floor_segments[hero_position].data = Empty { glyph = '-', } } case .Combat: monster := &floor_segments[hero_position].data.(Monster) if hero_attacking { message_history[message_history_slot] = "Swing" message_history_slot = (message_history_slot + 1) % len(message_history) dmg := (hero.base_atk + hero.weapon_bonus) - monster.def monster.hp -= dmg if dmg > 1 else 1 if monster.hp <= 0 { monster.glyph = 'x' hero.xp += monster.xp_reward hero.state = .Walking message_history[message_history_slot] = "I'm victorious!" message_history_slot = (message_history_slot + 1) % len(message_history) break } } else { message_history[message_history_slot] = "Ouch!" message_history_slot = (message_history_slot + 1) % len(message_history) dmg := monster.atk - (hero.base_def + hero.armor_bonus) hero.hp -= dmg if dmg > 1 else 1 if hero.hp <= 0 { message_history[message_history_slot] = "DEATH" message_history_slot = (message_history_slot + 1) % len(message_history) hero.state = .Dead break } } hero_attacking = !hero_attacking case .Dead: // fmt.printf("\x1B[5B\x1b[2KYOU DIED!\n") // running = false message_history[message_history_slot] = "Respawning..." message_history_slot = (message_history_slot + 1) % len(message_history) deaths += 1 current_floor = 0 hero_position = 0 hero.hp = hero.max_hp hero.gold = 0 hero.weapon_tier = 0 hero.armor_tier = 0 hero.weapon_bonus = 0 hero.armor_bonus = 0 hero.xp = 0 hero.state = .Walking generate_floor(floor_segments[:], current_floor, floor_weights) continue } // NOTE: loop to add multiple levels if enough xp was gained if hero.xp >= hero.xp_to_level { for hero.xp >= hero.xp_to_level { hero.xp -= hero.xp_to_level hero.xp_to_level = hero.level * 10 hero.level += 1 hero.max_hp += 5 hero.hp = hero.max_hp hero.base_atk += 1 if hero.level % 2 == 0 { hero.base_def += 1 } } message_history[message_history_slot] = "☆ Level up! ☆" message_history_slot = (message_history_slot + 1) % len(message_history) } // RENDER (this should probably be stringbuilt instead of multiple printfs) fmt.printf("\x1b[2KFloor: %d (deaths: %d)\n", current_floor + 1, deaths) fmt.printf("\x1b[2KLevel: %d", hero.level) fmt.printf(" [") progress := int((f32(hero.xp) / f32(hero.xp_to_level)) * 10) for i in 0 ..< 10 { if i > progress { fmt.printf("-") } else if i < progress { fmt.printf("=") } else { fmt.printf(">") } } fmt.printf("]\n") fmt.printf("\x1b[2KHP: %d/%d, Gold: %d\n", hero.hp, hero.max_hp, hero.gold) fmt.printf( "\x1b[2KW: %d(+%d), A: %d(+%d)\n", hero.weapon_tier, hero.weapon_bonus, hero.armor_tier, hero.armor_bonus, ) fmt.printf( "\x1b[2KDMG: %d, ARM: %d\n", hero.base_atk + hero.weapon_bonus, hero.base_def + hero.armor_bonus, ) fmt.printf("\x1b[2K") for segment, i in floor_segments { if i == hero_position { fmt.printf("%c", hero.glyph) } else { switch v in segment.data { case Empty: fmt.printf("%c", segment.data.(Empty).glyph) case Monster: fmt.printf("%c", segment.data.(Monster).glyph) case Treasure: fmt.printf("%c", segment.data.(Treasure).glyph) } } } fmt.printf("\x1B[5A\x1b[0G") fmt.printf( "\x1b[40C\x1b[0m\x1b[38;5;27m╭─⟦ᚠ⟧──────────────────────────────────⟦ᚱ⟧─╮\n", ) current_message_history_slot := (message_history_slot + len(message_history) - 10) % len(message_history) for current_message_history_slot != message_history_slot { fmt.printf( "\x1b[40C\x1b[38;5;24m│\x1b[0m %40s \x1b[38;5;24m│\n", message_history[current_message_history_slot], ) current_message_history_slot = (current_message_history_slot + 1) % len(message_history) } fmt.printf( "\x1b[40C\x1b[38;5;21m╰─⟦ᚦ⟧──────────────────────────────────⟦ᚨ⟧─╯\n\x1b[0m", ) fmt.printf("\x1B[12A") last_tick = now } running = !console.should_quit() /*if console.wait_for_input(con) != .none { fmt.printf("KEY\n") }*/ time.sleep(time.Millisecond) } fmt.printf("\x1b[2KEND\n") }