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
|