main.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. package main
  2. import (
  3. _ "embed"
  4. "fmt"
  5. "log"
  6. "math/rand"
  7. "strings"
  8. "time"
  9. "github.com/gdamore/tcell/v3"
  10. "github.com/gdamore/tcell/v3/color"
  11. )
  12. type coords struct {
  13. x, y int
  14. }
  15. var fallingQuotes = []string{
  16. "IT DESCENDS.",
  17. "The sky provides.",
  18. "I sense tuna.",
  19. "Ah yes. The prophecy.",
  20. "Rain harder.",
  21. "Something edible approaches.",
  22. }
  23. var catchQuotes = []string{
  24. "Mine.",
  25. "Calculated.",
  26. "Too easy.",
  27. "As foretold.",
  28. "Nom.",
  29. "You may drop another.",
  30. }
  31. var missQuotes = []string{
  32. "This is sabotage.",
  33. "The sky betrayed me.",
  34. "I meant to let that one go.",
  35. "It lacked seasoning.",
  36. "Unacceptable.",
  37. }
  38. func randomQuote(pool []string) string {
  39. return pool[rand.Intn(len(pool))]
  40. }
  41. //go:embed assets/title/title_idle.txt
  42. var title_idle string
  43. //go:embed assets/title/title_blinking.txt
  44. var title_blinking string
  45. func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
  46. row := y1
  47. col := x1
  48. var width int
  49. for text != "" {
  50. text, width = s.Put(col, row, text, style)
  51. col += width
  52. if col >= x2 {
  53. row++
  54. col = x1
  55. }
  56. if row > y2 {
  57. break
  58. }
  59. if width == 0 {
  60. // incomplete grapheme at end of string
  61. break
  62. }
  63. }
  64. }
  65. func drawTitle(s tcell.Screen, x, padding_top, frame int, style tcell.Style, titles [][]string) int{
  66. lines := titles[frame]
  67. frame = (frame + 1) % 2
  68. // Draw Title
  69. xmax, ymax := s.Size()
  70. for l:= range lines{
  71. drawText(s,x, l+padding_top, xmax, ymax, style, lines[l])
  72. }
  73. return len(lines)
  74. }
  75. func DrawMainMenu(s tcell.Screen, defStyle tcell.Style, old_session bool) bool{
  76. cats := [][]string{
  77. strings.Split(string(title_idle), "\n"),
  78. strings.Split(string(title_blinking), "\n")}
  79. var first_item_menu string
  80. if (old_session){
  81. first_item_menu = "Continue"
  82. } else {
  83. first_item_menu = "Start New Game"
  84. }
  85. curr_menu_item := 0
  86. menu := true
  87. gap_title_menu := 3
  88. settings, credits := false, false
  89. frame := 0
  90. ticker := time.NewTicker(500 * time.Millisecond)
  91. defer ticker.Stop()
  92. // Event loop
  93. for {
  94. s.Clear()
  95. title_size := drawTitle(s, 10, 2, frame, defStyle, cats)
  96. xmax,ymax := s.Size()
  97. // Draw Menu
  98. if menu {
  99. drawText(s, 10, title_size+gap_title_menu, xmax, ymax, defStyle, "Use arrows or WASD to navigate. Press ENTER to select")
  100. switch curr_menu_item {
  101. case 0:
  102. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, "▶ "+first_item_menu)
  103. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings")
  104. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits")
  105. case 1:
  106. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu)
  107. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, "▶ Settings")
  108. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits")
  109. case 2:
  110. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu)
  111. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings")
  112. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, "▶ Credits")
  113. }
  114. }
  115. if credits {
  116. drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Abderrahmane Faiz")
  117. drawText(s, 10, title_size+gap_title_menu+1, xmax,ymax, defStyle, "\u001B]8;;https://afaiz.dev\u001B\\https://afaiz.dev\u001B]8;;\u001B\\")
  118. drawText(s, 10, title_size+gap_title_menu+2, xmax,ymax, defStyle, "ASCII Art found on https://emojicombos.com/cat")
  119. drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu")
  120. }
  121. if settings {
  122. drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Nothing to customize for now ^^'")
  123. drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu")
  124. }
  125. // Update screen
  126. s.Show()
  127. // Poll event (this can be in a select statement as well)
  128. select{
  129. case <- ticker.C:
  130. frame = (frame + 1) % 2
  131. continue
  132. case ev := <-s.EventQ():
  133. // Process event
  134. switch ev := ev.(type) {
  135. default:
  136. continue
  137. case *tcell.EventResize:
  138. s.Sync()
  139. case *tcell.EventKey:
  140. if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
  141. return false
  142. } else if (ev.Key() == tcell.KeyUp || ev.Str() == "w" || ev.Str() == "W") && menu {
  143. if curr_menu_item == 0 {
  144. curr_menu_item = 2
  145. } else {
  146. curr_menu_item = curr_menu_item - 1
  147. }
  148. } else if (ev.Key() == tcell.KeyDown || ev.Str() == "s" || ev.Str() == "S") && menu {
  149. curr_menu_item = (curr_menu_item + 1) % 3
  150. } else if ev.Key() == tcell.KeyEnter && credits {
  151. credits = false
  152. menu = true
  153. } else if ev.Key() == tcell.KeyEnter && settings {
  154. settings = false
  155. menu = true
  156. } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 2 {
  157. menu = false
  158. credits = true
  159. } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 1 {
  160. menu = false
  161. settings = true
  162. } else if ev.Key() == tcell.KeyEnter && curr_menu_item == 0 {
  163. return true
  164. }
  165. }
  166. }
  167. }
  168. }
  169. func DrawCat(s tcell.Screen, x1,y1,x2,y2 int, style tcell.Style, direction rune) {
  170. mycat := []string{" /\\_/\\","( o.o )", " > ^ <"}
  171. switch direction{
  172. case 'l':
  173. mycat[len(mycat)-1] = " < ^ <"
  174. case 'r':
  175. mycat[len(mycat)-1] = " > ^ >"
  176. default :
  177. mycat[len(mycat)-1] = " > ^ <"
  178. }
  179. for index,cat := range mycat{
  180. drawText(s, x1, y1 + index, x2,y2, style, cat)
  181. }
  182. }
  183. // x1,y1 ------- x2,y1
  184. // | |
  185. // | |
  186. // x1,y2 ------- x2,y2
  187. func GetFoodPositions(x1,y1,x2,y2,level int) []*coords{
  188. var res []*coords
  189. nb_food := 5 + level
  190. for i := range nb_food {
  191. x := rand.Intn((x2-1)-(x1+1)) + (x1+1)
  192. y := y1 - i*3
  193. food_pos := coords{x: x, y: y}
  194. res = append(res, &food_pos)
  195. }
  196. return res
  197. }
  198. func DrawFood(s tcell.Screen, y1, y2 int, food_positions []*coords, defStyle tcell.Style, decrement bool) {
  199. xmax, ymax := s.Size()
  200. for _, food_pos := range food_positions {
  201. if food_pos.y > y1 && food_pos.y < (y2-1) {
  202. drawText(s,food_pos.x,food_pos.y,xmax,ymax,defStyle,"@")
  203. }
  204. if food_pos.y < (y2-1) && decrement {
  205. food_pos.y += 1
  206. }
  207. }
  208. }
  209. func main() {
  210. // Initialize screen
  211. defStyle := tcell.StyleDefault.Background(color.Black).Foreground(color.NewHexColor(0xf3e5ab))
  212. s, err := tcell.NewScreen()
  213. if err != nil {
  214. log.Fatalf("%+v", err)
  215. }
  216. if err := s.Init(); err != nil {
  217. log.Fatalf("%+v", err)
  218. }
  219. s.SetStyle(defStyle)
  220. quit := func() {
  221. maybePanic := recover()
  222. s.Fini()
  223. if maybePanic != nil {
  224. panic(maybePanic)
  225. }
  226. }
  227. defer quit()
  228. // Main Menu
  229. start_game := DrawMainMenu(s, defStyle, false)
  230. if (!start_game){
  231. return
  232. }
  233. score := 0
  234. level := 1
  235. direction := 'n'
  236. x_offset := 0
  237. new_level := true
  238. var food_positions []*coords
  239. speed := 0
  240. message := randomQuote(fallingQuotes)
  241. duration := time.Duration(1000/level)
  242. ticker := time.NewTicker(duration * time.Millisecond)
  243. game_loop:
  244. go_to_menu := false
  245. for {
  246. if (go_to_menu){
  247. break
  248. }
  249. s.Clear()
  250. xmax,ymax := s.Size()
  251. length_factor := 5
  252. width_factor := 4
  253. // x1,y1 ------- x2,y1
  254. // | |
  255. // | |
  256. // x1,y2 ------- x2,y2
  257. x1 := xmax/width_factor
  258. x2 := 3*xmax/width_factor
  259. width_map := x2 - x1 + 1
  260. y1 := ymax/length_factor
  261. y2 := 4*ymax/length_factor
  262. length_map := y2 - y1 + 1
  263. // Draw Game Map Borders
  264. // upper border
  265. for i := range(width_map){
  266. drawText(s,x1+i, y1-1, xmax, ymax, defStyle, "=")
  267. }
  268. // bottom border
  269. for i := range(width_map){
  270. drawText(s,x1+i, y2, xmax, ymax, defStyle, "=")
  271. }
  272. // left border
  273. for i := range(length_map){
  274. drawText(s,x1, y1+i, xmax, ymax, defStyle, "|")
  275. }
  276. // right border
  277. for i := range(length_map){
  278. drawText(s,x2, y1+i, xmax, ymax, defStyle, "|")
  279. }
  280. position_cat_x := (x2 + x1)/2 + x_offset
  281. speed = 2 + (level * 0)
  282. DrawCat(s, position_cat_x, y2 - 3, xmax,ymax,defStyle, direction)
  283. if (new_level){
  284. food_positions = GetFoodPositions(x1,y1,x2,y2,level)
  285. new_level = false
  286. }
  287. DrawFood(s,y1,y2,food_positions,defStyle, false)
  288. // Draw UI
  289. drawText(s, x1, y1 - 6, xmax, ymax, defStyle, "Use arrows or AD to move. Use Up Arrow or W to speed up")
  290. drawText(s, x1, y1 - 5, xmax, ymax, defStyle, "Press P to Pause.")
  291. drawText(s, x1, y1 - 3, xmax, ymax, defStyle, fmt.Sprintf("Level: %d", level))
  292. drawText(s, x1, y1 - 2, xmax, ymax, defStyle, fmt.Sprintf("Score: %d", score))
  293. drawText(s, x1, y2 + 1, xmax, ymax, defStyle, fmt.Sprintf("Vanilla: \"%s\"", message))
  294. s.Show()
  295. defer ticker.Stop()
  296. select{
  297. case <- ticker.C:
  298. DrawFood(s,y1,y2,food_positions,defStyle,true)
  299. s.Sync()
  300. case ev := <- s.EventQ():
  301. // Process event
  302. switch ev := ev.(type) {
  303. case *tcell.EventResize:
  304. s.Sync()
  305. case *tcell.EventKey:
  306. if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
  307. return
  308. } else if (ev.Key() == tcell.KeyRight || ev.Str() == "d" || ev.Str() == "D") {
  309. direction = 'r'
  310. if (position_cat_x + speed < (x2-7)){
  311. x_offset += speed
  312. } else {
  313. x_offset = x2 - 7 - (x2 + x1)/2
  314. }
  315. } else if (ev.Key() == tcell.KeyLeft || ev.Str() == "a" || ev.Str() == "A") {
  316. direction = 'l'
  317. if (position_cat_x - speed > (x1+1)){
  318. x_offset -= speed
  319. } else {
  320. x_offset = (x1+1) - (x2 + x1)/2
  321. }
  322. } else if (ev.Key() == tcell.KeyUp || ev.Str() == "w" || ev.Str() == "W") {
  323. direction = 'n'
  324. } else if (ev.Str() == "p" || ev.Str() == "P"){
  325. go_to_menu = true
  326. }
  327. }
  328. }
  329. // collect_food
  330. for _, food_pos := range food_positions{
  331. if (food_pos.y == y2 - 2){
  332. if (position_cat_x <= food_pos.x && food_pos.x <= position_cat_x + 7) {
  333. message = randomQuote(catchQuotes)
  334. score++
  335. } else {
  336. message = randomQuote(missQuotes)
  337. }
  338. }
  339. }
  340. new_level = true
  341. level++
  342. for _,food_pos := range food_positions{
  343. if food_pos.y < y2-3{
  344. level--
  345. new_level = false
  346. break
  347. }
  348. }
  349. }
  350. start_game = DrawMainMenu(s, defStyle, true)
  351. if (start_game){
  352. goto game_loop
  353. }
  354. }