main.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. frame = (frame + 1) % 2
  97. xmax,ymax := s.Size()
  98. // Draw Menu
  99. if menu {
  100. drawText(s, 10, title_size+gap_title_menu, xmax, ymax, defStyle, "Use arrows or WASD to navigate. Press ENTER to select")
  101. switch curr_menu_item {
  102. case 0:
  103. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, "▶ "+first_item_menu)
  104. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings")
  105. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits")
  106. case 1:
  107. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu)
  108. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, "▶ Settings")
  109. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, " Credits")
  110. case 2:
  111. drawText(s, 10, title_size+gap_title_menu+2, xmax, ymax, defStyle, " "+first_item_menu)
  112. drawText(s, 10, title_size+gap_title_menu+3, xmax, ymax, defStyle, " Settings")
  113. drawText(s, 10, title_size+gap_title_menu+4, xmax, ymax, defStyle, "▶ Credits")
  114. }
  115. }
  116. if credits {
  117. drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Abderrahmane Faiz")
  118. drawText(s, 10, title_size+gap_title_menu+1, xmax,ymax, defStyle, "\u001B]8;;https://afaiz.dev\u001B\\https://afaiz.dev\u001B]8;;\u001B\\")
  119. drawText(s, 10, title_size+gap_title_menu+2, xmax,ymax, defStyle, "ASCII Art found on https://emojicombos.com/cat")
  120. drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu")
  121. }
  122. if settings {
  123. drawText(s, 10, title_size+gap_title_menu, xmax,ymax, defStyle, "Nothing to customize for now ^^'")
  124. drawText(s, 10, title_size+gap_title_menu+4, xmax,ymax, defStyle, "Press ENTER to go back to the menu")
  125. }
  126. // Update screen
  127. s.Show()
  128. // Poll event (this can be in a select statement as well)
  129. select{
  130. case <- ticker.C:
  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) {
  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) {
  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. game_loop:
  242. go_to_menu := false
  243. for {
  244. if (go_to_menu){
  245. break
  246. }
  247. s.Clear()
  248. xmax,ymax := s.Size()
  249. length_factor := 5
  250. width_factor := 4
  251. // x1,y1 ------- x2,y1
  252. // | |
  253. // | |
  254. // x1,y2 ------- x2,y2
  255. x1 := xmax/width_factor
  256. x2 := 3*xmax/width_factor
  257. width_map := x2 - x1 + 1
  258. y1 := ymax/length_factor
  259. y2 := 4*ymax/length_factor
  260. length_map := y2 - y1 + 1
  261. // Draw Game Map Borders
  262. // upper border
  263. for i := range(width_map){
  264. drawText(s,x1+i, y1-1, xmax, ymax, defStyle, "=")
  265. }
  266. // bottom border
  267. for i := range(width_map){
  268. drawText(s,x1+i, y2, xmax, ymax, defStyle, "=")
  269. }
  270. // left border
  271. for i := range(length_map){
  272. drawText(s,x1, y1+i, xmax, ymax, defStyle, "|")
  273. }
  274. // right border
  275. for i := range(length_map){
  276. drawText(s,x2, y1+i, xmax, ymax, defStyle, "|")
  277. }
  278. position_cat_x := (x2 + x1)/2 + x_offset
  279. speed = 2 + (level * 0)
  280. DrawCat(s, position_cat_x, y2 - 3, xmax,ymax,defStyle, direction)
  281. if (new_level){
  282. food_positions = GetFoodPositions(x1,y1,x2,y2,level)
  283. new_level = false
  284. }
  285. DrawFood(s,y1,y2,food_positions,defStyle)
  286. // Draw UI
  287. drawText(s, x1, y1 - 6, xmax, ymax, defStyle, "Use arrows or AD to move. Use Up Arrow or W to speed up")
  288. drawText(s, x1, y1 - 5, xmax, ymax, defStyle, "Press P to Pause.")
  289. drawText(s, x1, y1 - 3, xmax, ymax, defStyle, fmt.Sprintf("Level: %d", level))
  290. drawText(s, x1, y1 - 2, xmax, ymax, defStyle, fmt.Sprintf("Score: %d", score))
  291. drawText(s, x1, y2 + 1, xmax, ymax, defStyle, fmt.Sprintf("Vanilla: \"%s\"", message))
  292. s.Show()
  293. ticker := time.NewTicker((1000) * time.Millisecond)
  294. defer ticker.Stop()
  295. select{
  296. case <- ticker.C:
  297. DrawFood(s,y1,y2,food_positions,defStyle)
  298. s.Sync()
  299. case ev := <- s.EventQ():
  300. // Process event
  301. switch ev := ev.(type) {
  302. case *tcell.EventResize:
  303. s.Sync()
  304. case *tcell.EventKey:
  305. if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
  306. return
  307. } else if (ev.Key() == tcell.KeyRight || ev.Str() == "d" || ev.Str() == "D") {
  308. direction = 'r'
  309. if (position_cat_x + speed < (x2-7)){
  310. x_offset += speed
  311. } else {
  312. x_offset = x2 - 7 - (x2 + x1)/2
  313. }
  314. } else if (ev.Key() == tcell.KeyLeft || ev.Str() == "a" || ev.Str() == "A") {
  315. direction = 'l'
  316. if (position_cat_x - speed > (x1+1)){
  317. x_offset -= speed
  318. } else {
  319. x_offset = (x1+1) - (x2 + x1)/2
  320. }
  321. } else if (ev.Key() == tcell.KeyUp || ev.Str() == "w" || ev.Str() == "W") {
  322. direction = 'n'
  323. } else if (ev.Str() == "p" || ev.Str() == "P"){
  324. go_to_menu = true
  325. }
  326. }
  327. }
  328. // collect_food
  329. for _, food_pos := range food_positions{
  330. if (food_pos.y == y2 - 2){
  331. if (position_cat_x <= food_pos.x && food_pos.x <= position_cat_x + 7) {
  332. message = randomQuote(catchQuotes)
  333. score++
  334. } else {
  335. message = randomQuote(missQuotes)
  336. }
  337. }
  338. }
  339. new_level = true
  340. level++
  341. for _,food_pos := range food_positions{
  342. if food_pos.y < y2-3{
  343. level--
  344. new_level = false
  345. break
  346. }
  347. }
  348. }
  349. start_game = DrawMainMenu(s, defStyle, true)
  350. if (start_game){
  351. goto game_loop
  352. }
  353. }