Download
134 lines 3.9 KiBTomoModified 2025-12-02 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
# A terminal game of Snake

# Library imports
# BTUI is a terminal library: ~/software/tomo-btui/btui.tm
btui := use btui
# Random is an RNG: ~/software/tomo-random/random.tm
use random
# Modules are sourced from ./modules.ini


# Main function has automatic command line argument parsing:
func main(snake_color|C:Color=Green, fruits|f=10)
    # Set up terminal interface
    btui.set_mode(TUI)
    at_cleanup(btui.disable)
    repeat
        # Create the world
        world := World(
            Snek([ScreenVec2(10, 10)], color=snake_color),
            num_fruits=fruits,
        )
        world.update()
        world.redraw()

        # Game loop
        while not world.snek.is_dead()
            key := btui.get_key(timeout=1)
            when key is "Q", "Ctrl-c"
                exit(code=0)
            else
                world.snek.handle_input(key)
            world.update()

        # Game over
        btui.style(bg=Red, fg=Black)
        btui.write(" GAME OVER ", btui.get_size()/2, Center)
        btui.write(" PRESS ANY KEY TO CONTINUE ", btui.get_size()/2 + ScreenVec2(0,1), Center)
        when btui.get_key() is "Q", "Ctrl-c"
            exit(code=0)


struct Snek(
    segments:[ScreenVec2],
    color:Color=Green,
    facing:ScreenVec2=ScreenVec2(2,0),
    length:Int=10,
)
    func draw(self:Snek)
        style(bg=self.color)
        for segment in self.segments
            write("  ", segment)

    func update(self:&Snek)
        # Move forward
        new_head := self.segments[-1]! + self.facing
        (&self.segments).insert(new_head)

        # Clean up butt
        if self.segments.length > self.length
            style(bg=Normal)
            for old_pos in self.segments.to(-self.length-1)
                write("  ", old_pos)
            self.segments = self.segments.from(-self.length)

        # Draw new head
        style(fg=Normal, bg=self.color)
        write("  ", new_head)

    func is_dead(self:Snek -> Bool)
        size := btui.get_size()
        head := self.segments[-1]!
        # Dead if snake hits the edge of the screen
        if head.x < 0 or head.x >= size.x or head.y < 0 or head.y >= size.y
            return yes
        # Dead if snake collides with itself
        body := self.segments.to(-2)
        return (or: head == b for b in body) or no

    func handle_input(self:&Snek, key:Text)
        new_dir := (
            when key
            # x-coordinates are doubled to make text "pixels" square
            is "a","Left" then ScreenVec2(-2, 0)
            is "d","Right" then ScreenVec2(2, 0)
            is "w","Up" then ScreenVec2(0, -1)
            is "s","Down" then ScreenVec2(0, 1)
            else none
        )
        if dir := new_dir
            if self.segments[-1]! + dir != self.segments[-2]
                self.facing = dir


struct World(
    snek:Snek,
    fruits:{ScreenVec2:Text}={},
    num_fruits:Int=1000,
)
    FRUITS := "πŸ…πŸ†πŸ‡πŸˆπŸ‰πŸŠπŸ‹πŸŒπŸπŸŽπŸπŸπŸ‘πŸ’πŸ“".split()

    func redraw(self:World)
        btui.style(bg=Normal, fg=Normal)
        btui.clear()

        self.snek.draw()

        btui.style(bg=Normal, fg=Normal)
        for pos,fruit in self.fruits
            btui.write(fruit, pos)

    func update(self:&World)
        self.snek.update()

        # Eat fruit
        head_pos := self.snek.segments[-1]!
        if self.fruits.has(head_pos)
            self.snek.length += 1
            (&self.fruits).remove(head_pos)
        
        # Spawn new fruit till limit
        while self.fruits.length < self.num_fruits
            self.spawn_fruit()

    func spawn_fruit(self:&World)
        size := btui.get_size()
        repeat
            # Random fruit and position
            fruit := World.FRUITS.random()
            pos := ScreenVec2(2*random.int(0, size.x/2), random.int(0, size.y-1))
            if not self.snek.segments.has(pos)
                (&self.fruits)[pos] = fruit
                btui.style(bg=Normal, fg=Normal)
                btui.write(fruit, pos)
                return