package main import ( _ "embed" "fmt" "log" "math/rand" "strings" "time" "github.com/gdamore/tcell/v3" "github.com/gdamore/tcell/v3/color" ) type coords struct { x, y int } var fallingQuotes = []string{ "IT DESCENDS.", "The sky provides.", "I sense tuna.", "Ah yes. The prophecy.", "Rain harder.", "Something edible approaches.", } var catchQuotes = []string{ "Mine.", "Calculated.", "Too easy.", "As foretold.", "Nom.", "You may drop another.", } var missQuotes = []string{ "This is sabotage.", "The sky betrayed me.", "I meant to let that one go.", "It lacked seasoning.", "Unacceptable.", } func randomQuote(pool []string) string { return pool[rand.Intn(len(pool))] } //go:embed assets/title/title_idle.txt var title_idle string //go:embed assets/title/title_blinking.txt var title_blinking string func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) { row := y1 col := x1 var width int for text != "" { text, width = s.Put(col, row, text, style) col += width if col >= x2 { row++ col = x1 } if row > y2 { break } if width == 0 { // incomplete grapheme at end of string break } } } func drawTitle(s tcell.Screen, x, padding_top, frame int, style tcell.Style, titles [][]string) int{ lines := titles[frame] frame = (frame + 1) % 2 // Draw Title xmax, ymax := s.Size() for l:= range lines{ drawText(s,x, l+padding_top, xmax, ymax, style, lines[l]) } return len(lines) } func DrawMainMenu(s tcell.Screen, defStyle tcell.Style, old_session bool) bool{ cats := [][]string{ strings.Split(string(title_idle), "\n"), strings.Split(string(title_blinking), "\n")} var first_item_menu string if (old_session){ first_item_menu = "Continue" } else { first_item_menu = "Start New Game" } curr_menu_item := 0 menu := true gap_title_menu := 3 settings, credits := false, false frame := 0 ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() // Event loop for { s.Clear() title_size := drawTitle(s, 10, 2, frame, defStyle, cats) frame = (frame + 1) % 2 xmax,ymax := s.Size() // Draw Menu if menu { drawText(s, 10, title_size+gap_title_menu, xmax, ymax, defStyle, "Use arrows or WASD to navigate. Press ENTER to select") switch curr_menu_item { case 0: drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, "▶ "+first_item_menu) drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings") drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits") case 1: drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu) drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, "▶ Settings") drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits") case 2: drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu) drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings") drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, "▶ Credits") } } if credits { drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Abderrahmane Faiz") drawText(s, 10, title_size+gap_title_menu+1, xmax,ymax, defStyle, "\u001B]8;;https://afaiz.dev\u001B\\https://afaiz.dev\u001B]8;;\u001B\\") drawText(s, 10, title_size+gap_title_menu+2, xmax,ymax, defStyle, "ASCII Art found on https://emojicombos.com/cat") drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu") } if settings { drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Nothing to customize for now ^^'") drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu") } // Update screen s.Show() // Poll event (this can be in a select statement as well) select{ case <- ticker.C: continue case ev := <-s.EventQ(): // Process event switch ev := ev.(type) { default: continue case *tcell.EventResize: s.Sync() case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { return false } else if (ev.Key() == tcell.KeyUp || ev.Str() == "w" || ev.Str() == "W") && menu { if curr_menu_item == 0 { curr_menu_item = 2 } else { curr_menu_item = curr_menu_item - 1 } } else if (ev.Key() == tcell.KeyDown || ev.Str() == "s" || ev.Str() == "S") && menu { curr_menu_item = (curr_menu_item + 1) % 3 } else if ev.Key() == tcell.KeyEnter && credits { credits = false menu = true } else if ev.Key() == tcell.KeyEnter && settings { settings = false menu = true } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 2 { menu = false credits = true } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 1 { menu = false settings = true } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 0 { return true } } } } } func DrawCat(s tcell.Screen, x1,y1,x2,y2 int, style tcell.Style, direction rune) { mycat := []string{" /\\_/\\","( o.o )", " > ^ <"} switch direction{ case 'l': mycat[len(mycat)-1] = " < ^ <" case 'r': mycat[len(mycat)-1] = " > ^ >" default : mycat[len(mycat)-1] = " > ^ <" } for index,cat := range mycat{ drawText(s, x1, y1 + index, x2,y2, style, cat) } } // x1,y1 ------- x2,y1 // | | // | | // x1,y2 ------- x2,y2 func GetFoodPositions(x1,y1,x2,y2,level int) []*coords{ var res []*coords nb_food := 5 + level for i := range nb_food { x := rand.Intn((x2-1)-(x1+1)) + (x1+1) y := y1 - i*3 food_pos := coords{x: x, y: y} res = append(res, &food_pos) } return res } func DrawFood(s tcell.Screen, y1, y2 int, food_positions []*coords, defStyle tcell.Style) { xmax, ymax := s.Size() for _, food_pos := range food_positions { if food_pos.y > y1 && food_pos.y < (y2-1) { drawText(s,food_pos.x,food_pos.y,xmax,ymax,defStyle,"@") } if food_pos.y < (y2-1) { food_pos.y += 1 } } } func main() { // Initialize screen defStyle := tcell.StyleDefault.Background(color.Black).Foreground(color.NewHexColor(0xf3e5ab)) s, err := tcell.NewScreen() if err != nil { log.Fatalf("%+v", err) } if err := s.Init(); err != nil { log.Fatalf("%+v", err) } s.SetStyle(defStyle) quit := func() { maybePanic := recover() s.Fini() if maybePanic != nil { panic(maybePanic) } } defer quit() // Main Menu start_game := DrawMainMenu(s, defStyle, false) if (!start_game){ return } score := 0 level := 1 direction := 'n' x_offset := 0 new_level := true var food_positions []*coords speed := 0 message := randomQuote(fallingQuotes) game_loop: go_to_menu := false for { if (go_to_menu){ break } s.Clear() xmax,ymax := s.Size() length_factor := 5 width_factor := 4 // x1,y1 ------- x2,y1 // | | // | | // x1,y2 ------- x2,y2 x1 := xmax/width_factor x2 := 3*xmax/width_factor width_map := x2 - x1 + 1 y1 := ymax/length_factor y2 := 4*ymax/length_factor length_map := y2 - y1 + 1 // Draw Game Map Borders // upper border for i := range(width_map){ drawText(s,x1+i, y1-1, xmax, ymax, defStyle, "=") } // bottom border for i := range(width_map){ drawText(s,x1+i, y2, xmax, ymax, defStyle, "=") } // left border for i := range(length_map){ drawText(s,x1, y1+i, xmax, ymax, defStyle, "|") } // right border for i := range(length_map){ drawText(s,x2, y1+i, xmax, ymax, defStyle, "|") } position_cat_x := (x2 + x1)/2 + x_offset speed = 2 + (level * 0) DrawCat(s, position_cat_x, y2 - 3, xmax,ymax,defStyle, direction) if (new_level){ food_positions = GetFoodPositions(x1,y1,x2,y2,level) new_level = false } DrawFood(s,y1,y2,food_positions,defStyle) // Draw UI drawText(s, x1, y1 - 6, xmax, ymax, defStyle, "Use arrows or AD to move. Use Up Arrow or W to speed up") drawText(s, x1, y1 - 5, xmax, ymax, defStyle, "Press P to Pause.") drawText(s, x1, y1 - 3, xmax, ymax, defStyle, fmt.Sprintf("Level: %d", level)) drawText(s, x1, y1 - 2, xmax, ymax, defStyle, fmt.Sprintf("Score: %d", score)) drawText(s, x1, y2 + 1, xmax, ymax, defStyle, fmt.Sprintf("Vanilla: \"%s\"", message)) s.Show() ticker := time.NewTicker((1000) * time.Millisecond) defer ticker.Stop() select{ case <- ticker.C: DrawFood(s,y1,y2,food_positions,defStyle) s.Sync() case ev := <- s.EventQ(): // Process event switch ev := ev.(type) { case *tcell.EventResize: s.Sync() case *tcell.EventKey: if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC { return } else if (ev.Key() == tcell.KeyRight || ev.Str() == "d" || ev.Str() == "D") { direction = 'r' if (position_cat_x + speed < (x2-7)){ x_offset += speed } else { x_offset = x2 - 7 - (x2 + x1)/2 } } else if (ev.Key() == tcell.KeyLeft || ev.Str() == "a" || ev.Str() == "A") { direction = 'l' if (position_cat_x - speed > (x1+1)){ x_offset -= speed } else { x_offset = (x1+1) - (x2 + x1)/2 } } else if (ev.Key() == tcell.KeyUp || ev.Str() == "w" || ev.Str() == "W") { direction = 'n' } else if (ev.Str() == "p" || ev.Str() == "P"){ go_to_menu = true } } } // collect_food for _, food_pos := range food_positions{ if (food_pos.y == y2 - 2){ if (position_cat_x <= food_pos.x && food_pos.x <= position_cat_x + 7) { message = randomQuote(catchQuotes) score++ } else { message = randomQuote(missQuotes) } } } new_level = true level++ for _,food_pos := range food_positions{ if food_pos.y < y2-3{ level-- new_level = false break } } } start_game = DrawMainMenu(s, defStyle, true) if (start_game){ goto game_loop } }