github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/game.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "image" 7 "image/draw" 8 "image/png" 9 "math" 10 "math/rand" 11 "os" 12 "os/signal" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "syscall" 17 "time" 18 19 "github.com/xyproto/files" 20 "github.com/xyproto/vt100" 21 ) 22 23 // There is a tradition for including silly little games in editors, so here goes: 24 25 const ( 26 bobRuneLarge = 'O' 27 bobRuneSmall = 'o' 28 evilGobblerRune = '€' 29 bubbleRune = '°' 30 gobblerRune = 'G' 31 gobblerDeadRune = 'T' 32 gobblerZombieRune = '@' 33 bobWonRune = 'Y' 34 bobLostRune = 'n' 35 pelletRune = '¤' 36 ) 37 38 var ( 39 highScoreFile = filepath.Join(userCacheDir, "o", "highscore.txt") 40 41 bobColor = vt100.LightYellow 42 bobWonColor = vt100.LightGreen 43 bobLostColor = vt100.Red 44 evilGobblerColor = vt100.LightRed 45 gobblerColor = vt100.Yellow 46 gobblerDeadColor = vt100.DarkGray 47 gobblerZombieColor = vt100.LightBlue 48 bubbleColor = vt100.Magenta 49 pelletColor1 = vt100.LightGreen 50 pelletColor2 = vt100.Green 51 statusTextColor = vt100.Black 52 statusTextBackground = vt100.Blue 53 resizeColor = vt100.LightMagenta 54 gameBackgroundColor = vt100.DefaultBackground 55 ) 56 57 // Bob represents the player 58 type Bob struct { 59 color vt100.AttributeColor // foreground color 60 x, y int // current position 61 oldx, oldy int // previous position 62 w, h float64 63 state rune // looks 64 } 65 66 // NewBob creates a new Bob struct 67 func NewBob(c *vt100.Canvas, startingWidth int) *Bob { 68 return &Bob{ 69 x: startingWidth / 20, 70 y: 10, 71 oldx: startingWidth / 20, 72 oldy: 10, 73 state: bobRuneSmall, 74 color: bobColor, 75 w: float64(c.W()), 76 h: float64(c.H()), 77 } 78 } 79 80 // ToggleState changes the look of Bob as he moves 81 func (b *Bob) ToggleState() { 82 const up = bobRuneLarge 83 const down = bobRuneSmall 84 if b.state == up { 85 b.state = down 86 } else { 87 b.state = up 88 } 89 } 90 91 // Draw is called when Bob should be drawn on the canvas 92 func (b *Bob) Draw(c *vt100.Canvas) { 93 c.PlotColor(uint(b.x), uint(b.y), b.color, b.state) 94 } 95 96 // Right is called when Bob should move right 97 func (b *Bob) Right() bool { 98 oldx := b.x 99 b.x++ 100 if b.x >= int(b.w) { 101 b.x-- 102 return false 103 } 104 b.oldx = oldx 105 b.oldy = b.y 106 return true 107 } 108 109 // Left is called when Bob should move left 110 func (b *Bob) Left() bool { 111 oldx := b.x 112 if b.x-1 < 0 { 113 return false 114 } 115 b.x-- 116 b.oldx = oldx 117 b.oldy = b.y 118 return true 119 } 120 121 // Up is called when Bob should move up 122 func (b *Bob) Up() bool { 123 oldy := b.y 124 if b.y-1 <= 0 { 125 return false 126 } 127 b.y-- 128 b.oldx = b.x 129 b.oldy = oldy 130 return true 131 } 132 133 // Down is called when Bob should move down 134 func (b *Bob) Down(c *vt100.Canvas) bool { 135 oldy := b.y 136 b.y++ 137 if b.y >= int(c.H()) { 138 b.y-- 139 return false 140 } 141 b.oldx = b.x 142 b.oldy = oldy 143 return true 144 } 145 146 // Resize is called when the terminal is resized 147 func (b *Bob) Resize(c *vt100.Canvas) { 148 b.color = resizeColor 149 b.w = float64(c.W()) 150 b.h = float64(c.H()) 151 } 152 153 // Pellet represents a pellet that can both feed Gobblers and hit the EvilGobbler 154 type Pellet struct { 155 color vt100.AttributeColor // foreground color 156 lifeCounter int 157 oldx, oldy int // previous position 158 vx, vy int // velocity 159 x, y int // current position 160 w, h float64 161 state rune // looks 162 removed bool // to be removed 163 stopped bool // is the movement stopped? 164 } 165 166 // NewPellet creates a new Pellet struct, with position and speed 167 func NewPellet(c *vt100.Canvas, x, y, vx, vy int) *Pellet { 168 return &Pellet{ 169 x: x, 170 y: y, 171 oldx: x, 172 oldy: y, 173 vx: vx, 174 vy: vy, 175 state: pelletRune, 176 color: pelletColor1, 177 stopped: false, 178 removed: false, 179 lifeCounter: 0, 180 w: float64(c.W()), 181 h: float64(c.H()), 182 } 183 } 184 185 // ToggleColor will alternate the colors for this Pellet 186 func (b *Pellet) ToggleColor() { 187 c1 := pelletColor1 188 c2 := pelletColor2 189 if b.color.Equal(c1) { 190 b.color = c2 191 } else { 192 b.color = c1 193 } 194 } 195 196 // Draw draws the Pellet on the canvas 197 func (b *Pellet) Draw(c *vt100.Canvas) { 198 c.PlotColor(uint(b.x), uint(b.y), b.color, b.state) 199 } 200 201 // Next moves the object to the next position, and returns true if it moved 202 func (b *Pellet) Next(c *vt100.Canvas, e *EvilGobbler) bool { 203 b.lifeCounter++ 204 if b.lifeCounter > 20 { 205 b.removed = true 206 b.ToggleColor() 207 return false 208 } 209 210 if b.stopped { 211 b.ToggleColor() 212 return false 213 } 214 if b.x-b.vx < 0 { 215 b.ToggleColor() 216 return false 217 } 218 if b.y-b.vy < 0 { 219 b.ToggleColor() 220 return false 221 } 222 223 b.oldx = b.x 224 b.oldy = b.y 225 226 b.x += b.vx 227 b.y += b.vy 228 229 if b.x == e.x && b.y == e.y { 230 e.shot = true 231 } 232 233 if b.HitSomething(c) { 234 b.x = b.oldx 235 b.y = b.oldy 236 return false 237 } 238 if b.x >= int(b.w) || b.x < 0 { 239 b.x -= b.vx 240 return false 241 } 242 if b.y >= int(c.H()) { 243 b.y -= b.vy 244 return false 245 } else if b.y <= 0 { 246 b.y -= b.vy 247 return false 248 } 249 return true 250 } 251 252 // Stop is called when the pellet should stop moving 253 func (b *Pellet) Stop() { 254 b.vx = 0 255 b.vy = 0 256 b.stopped = true 257 } 258 259 // HitSomething is called when the pellet hits something 260 func (b *Pellet) HitSomething(c *vt100.Canvas) bool { 261 r, err := c.At(uint(b.x), uint(b.y)) 262 if err != nil { 263 return false 264 } 265 if r != rune(0) && r != ' ' { 266 // Hit something. Check the next-next position too 267 r2, err := c.At(uint(b.x+b.vx), uint(b.y+b.vy)) 268 if err != nil { 269 return true 270 } 271 if r2 != rune(0) && r2 != ' ' { 272 b.Stop() 273 } 274 return true 275 } 276 return false 277 } 278 279 // Resize is called when the terminal is resized 280 func (b *Pellet) Resize(c *vt100.Canvas) { 281 b.stopped = false 282 b.w = float64(c.W()) 283 b.h = float64(c.H()) 284 } 285 286 // Bubble represents a bubble character that is in the way 287 type Bubble struct { 288 color vt100.AttributeColor // foreground color 289 x, y int // current position 290 oldx, oldy int // previous position 291 w, h float64 292 state rune // looks 293 } 294 295 // NewBubbles creates n new Bubble structs 296 func NewBubbles(c *vt100.Canvas, startingWidth int, n int) []*Bubble { 297 bubbles := make([]*Bubble, n) 298 for i := range bubbles { 299 bubbles[i] = NewBubble(c, startingWidth) 300 } 301 return bubbles 302 } 303 304 // NewBubble creates a new Bubble struct 305 func NewBubble(c *vt100.Canvas, startingWidth int) *Bubble { 306 return &Bubble{ 307 x: startingWidth / 5, 308 y: 10, 309 oldx: startingWidth / 5, 310 oldy: 10, 311 state: bubbleRune, 312 color: bubbleColor, 313 w: float64(c.W()), 314 h: float64(c.H()), 315 } 316 } 317 318 // Draw draws the Bubble on the canvas 319 func (b *Bubble) Draw(c *vt100.Canvas) { 320 c.PlotColor(uint(b.x), uint(b.y), b.color, b.state) 321 } 322 323 // Resize is called when the terminal is resized 324 func (b *Bubble) Resize(c *vt100.Canvas) { 325 b.color = resizeColor 326 b.w = float64(c.W()) 327 b.h = float64(c.H()) 328 } 329 330 // Next moves the object to the next position, and returns true if it moved 331 func (b *Bubble) Next(c *vt100.Canvas, bob *Bob, gobblers *[]*Gobbler) bool { 332 b.oldx = b.x 333 b.oldy = b.y 334 335 d := distance(bob.x, b.x, bob.y, b.y) 336 if d > 10 { 337 if b.x < bob.x { 338 b.x++ 339 } else if b.x > bob.x { 340 b.x-- 341 } 342 if b.y < bob.y { 343 b.y++ 344 } else if b.y > bob.y { 345 b.y-- 346 } 347 } else { 348 for { 349 dx := b.x - b.oldx 350 dy := b.y - b.oldy 351 b.x += int(math.Round(float64(dx*3+rand.Intn(5)-2) / float64(4))) // -2, -1, 0, 1, 2 352 b.y += int(math.Round(float64(dy*3+rand.Intn(5)-2) / float64(4))) 353 if b.x != b.oldx { 354 break 355 } 356 if b.y != b.oldy { 357 break 358 } 359 } 360 } 361 362 if b.HitSomething(c) { 363 // "Wake up" dead gobblers 364 for _, g := range *gobblers { 365 if g.x == b.x && g.y == b.y { 366 if g.dead { 367 g.dead = false 368 g.state = gobblerZombieRune 369 g.color = gobblerZombieColor 370 } 371 } 372 } 373 // step back 374 b.x = b.oldx 375 b.y = b.oldy 376 return false 377 } 378 379 if b.x >= int(b.w) { 380 b.x = b.oldx 381 } else if b.x <= 0 { 382 b.x = b.oldx 383 } 384 if b.y >= int(c.H()) { 385 b.y = b.oldy 386 } else if b.y <= 0 { 387 b.y = b.oldy 388 } 389 390 return b.x != b.oldx || b.y != b.oldy 391 } 392 393 // HitSomething is called if the Bubble hits another character 394 func (b *Bubble) HitSomething(c *vt100.Canvas) bool { 395 r, err := c.At(uint(b.x), uint(b.y)) 396 if err != nil { 397 return false 398 } 399 // Hit something? 400 return r != rune(0) && r != ' ' 401 } 402 403 // EvilGobbler is a character that hunts Gobblers 404 type EvilGobbler struct { 405 hunting *Gobbler 406 color vt100.AttributeColor // foreground color 407 x, y int // current position 408 oldx, oldy int // previous position 409 counter uint 410 huntingDistance float64 411 w, h float64 412 state rune // looks 413 shot bool 414 } 415 416 // NewEvilGobbler creates an EvilGobbler struct. 417 // startingWidth is the initial width of the canvas. 418 func NewEvilGobbler(c *vt100.Canvas, startingWidth int) *EvilGobbler { 419 return &EvilGobbler{ 420 x: startingWidth/2 + 5, 421 y: 0o1, 422 oldx: startingWidth/2 + 5, 423 oldy: 10, 424 state: evilGobblerRune, 425 color: evilGobblerColor, 426 counter: 0, 427 shot: false, 428 hunting: nil, 429 huntingDistance: 0.0, 430 w: float64(c.W()), 431 h: float64(c.H()), 432 } 433 } 434 435 // Draw will draw the EvilGobbler on the canvas 436 func (e *EvilGobbler) Draw(c *vt100.Canvas) { 437 c.PlotColor(uint(e.x), uint(e.y), e.color, e.state) 438 } 439 440 // Next will make the next EvilGobbler move 441 func (e *EvilGobbler) Next(c *vt100.Canvas, gobblers *[]*Gobbler) bool { 442 e.oldx = e.x 443 e.oldy = e.y 444 445 minDistance := 0.0 446 found := false 447 for i, g := range *gobblers { 448 if d := distance(g.x, g.y, e.x, e.y); !g.dead && (d < minDistance || minDistance == 0.0) { 449 e.hunting = (*gobblers)[i] 450 minDistance = d 451 found = true 452 } 453 } 454 if found { 455 e.huntingDistance = minDistance 456 } 457 458 if e.hunting == nil { 459 460 e.x += rand.Intn(3) - 1 461 e.y += rand.Intn(3) - 1 462 463 } else { 464 465 xspeed := 1 466 yspeed := 1 467 468 if e.x < e.hunting.x { 469 e.x += xspeed 470 } else if e.x > e.hunting.x { 471 e.x -= xspeed 472 } 473 if e.y < e.hunting.y { 474 e.y += yspeed 475 } else if e.y > e.hunting.y { 476 e.y -= yspeed 477 } 478 479 if !e.hunting.dead && e.huntingDistance < 1.8 || (e.hunting.x == e.x && e.hunting.y == e.y) { 480 e.hunting.dead = true 481 e.counter++ 482 e.hunting = nil 483 e.huntingDistance = 9999.9 484 } 485 } 486 487 if e.x > int(e.w) { 488 e.x = e.oldx 489 } else if e.x < 0 { 490 e.x = e.oldx 491 } 492 493 if e.y > int(c.H()) { 494 e.y = e.oldy 495 } else if e.y <= 0 { 496 e.y = e.oldy 497 } 498 499 return (e.x != e.oldx || e.y != e.oldy) 500 } 501 502 // Resize is called when the terminal is resized 503 func (e *EvilGobbler) Resize(c *vt100.Canvas) { 504 e.color = resizeColor 505 e.w = float64(c.W()) 506 e.h = float64(c.H()) 507 } 508 509 // Gobbler represents a character that can move around and eat pellets 510 type Gobbler struct { 511 hunting *Pellet // current pellet to hunt 512 color vt100.AttributeColor // foreground color 513 x, y int // current position 514 oldx, oldy int // previous position 515 huntingDistance float64 // how far to closest pellet 516 counter uint 517 w, h float64 518 state rune // looks 519 dead bool 520 } 521 522 // NewGobbler creates a new Gobbler struct 523 func NewGobbler(c *vt100.Canvas, startingWidth int) *Gobbler { 524 return &Gobbler{ 525 x: startingWidth / 2, 526 y: 10, 527 oldx: startingWidth / 2, 528 oldy: 10, 529 state: gobblerRune, 530 color: gobblerColor, 531 hunting: nil, 532 huntingDistance: 0, 533 counter: 0, 534 dead: false, 535 w: float64(c.W()), 536 h: float64(c.H()), 537 } 538 } 539 540 // NewGobblers creates n new Gobbler structs 541 func NewGobblers(c *vt100.Canvas, startingWidth int, n int) []*Gobbler { 542 gobblers := make([]*Gobbler, n) 543 for i := range gobblers { 544 gobblers[i] = NewGobbler(c, startingWidth) 545 } 546 return gobblers 547 } 548 549 // Draw draws the current Gobbler on the canvas 550 func (g *Gobbler) Draw(c *vt100.Canvas) { 551 c.PlotColor(uint(g.x), uint(g.y), g.color, g.state) 552 } 553 554 // Next is called when the next move should be made 555 func (g *Gobbler) Next(pellets *[]*Pellet, bob *Bob) bool { 556 if g.dead { 557 return false 558 } 559 560 g.oldx = g.x 561 g.oldy = g.y 562 563 xspeed := 1 564 yspeed := 1 565 566 // Move to the nearest pellet and eat it 567 if len(*pellets) == 0 { 568 569 g.x += rand.Intn(5) - 2 570 g.y += rand.Intn(5) - 2 571 572 } else { 573 574 if g.hunting == nil || g.hunting.removed { 575 minDistance := 0.0 576 var closestPellet *Pellet 577 578 // TODO: Hunt a random pellet that is not already hunted instead of the closest 579 580 for i, b := range *pellets { 581 if d := distance(b.x, b.y, g.x, g.y); !b.removed && (minDistance == 0.0 || d < minDistance) { 582 closestPellet = (*pellets)[i] 583 minDistance = d 584 } 585 } 586 if closestPellet != nil { 587 g.hunting = closestPellet 588 g.huntingDistance = minDistance 589 } 590 } else { 591 g.huntingDistance = distance(g.hunting.x, g.hunting.y, g.x, g.y) 592 } 593 594 if g.hunting == nil { 595 596 g.x += rand.Intn(3) - 1 597 g.y += rand.Intn(3) - 1 598 599 } else { 600 601 if abs(g.hunting.x-g.x) >= abs(g.hunting.y-g.y) { 602 // Longer away along x than along y 603 if g.huntingDistance > 10 { 604 xspeed = 3 605 yspeed = 2 606 } else if g.huntingDistance > 5 { 607 xspeed = 2 + rand.Intn(2) 608 yspeed = 2 609 } 610 } else { 611 // Longer away along x than along y 612 if g.huntingDistance > 10 { 613 xspeed = 2 614 yspeed = 3 615 } else if g.huntingDistance > 5 { 616 xspeed = 2 617 yspeed = 2 + rand.Intn(2) 618 } 619 } 620 621 if g.x < g.hunting.x { 622 g.x += xspeed 623 } else if g.x > g.hunting.x { 624 g.x -= xspeed 625 } 626 if g.y < g.hunting.y { 627 g.y += yspeed 628 } else if g.y > g.hunting.y { 629 g.y -= yspeed 630 } 631 632 if distance(bob.x, bob.y, g.x, g.y) < 4 { 633 g.x = g.oldx + (rand.Intn(3) - 1) 634 g.y = g.oldy + (rand.Intn(3) - 1) 635 } 636 637 if !g.hunting.removed && g.huntingDistance < 2 || (g.hunting.x == g.x && g.hunting.y == g.y) { 638 g.hunting.removed = true 639 g.counter++ 640 g.hunting = nil 641 g.huntingDistance = 9999.9 642 } 643 } 644 } 645 646 if g.x > int(g.w) { 647 g.x = int(g.w) - 1 648 g.x -= xspeed 649 } else if g.x < 0 { 650 g.x = 0 651 g.x += xspeed 652 } 653 654 if g.y > int(g.h) { 655 g.y = int(g.h) - 1 656 g.y -= yspeed 657 } else if g.y <= 0 { 658 g.y = 0 659 g.y += yspeed 660 } 661 662 if g.x <= 2 && g.y >= (int(g.h)-2) { 663 // Close to the lower left corner 664 g.x = int(g.w) - 1 // teleport! 665 g.y = 0 // teleport! 666 } else if g.x <= 2 && g.y <= 2 { 667 // Close to the upper left corner 668 g.x = int(g.w) - 1 // teleport! 669 g.y = int(g.h) - 1 // teleport 670 } 671 672 return (g.x != g.oldx || g.y != g.oldy) 673 } 674 675 // Resize is called when the terminal is resized 676 func (g *Gobbler) Resize(c *vt100.Canvas) { 677 g.color = resizeColor 678 g.w = float64(c.W()) 679 g.h = float64(c.H()) 680 } 681 682 // saveHighScore will save the given high score to a file, 683 // creating a new file if needed and overwriting the existing highscore 684 // if it's already there. 685 func saveHighScore(highScore uint) error { 686 if noWriteToCache { 687 return nil 688 } 689 // First create the folders, if needed 690 folderPath := filepath.Dir(highScoreFile) 691 os.MkdirAll(folderPath, os.ModePerm) 692 // Prepare the file 693 f, err := os.OpenFile(highScoreFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) 694 if err != nil { 695 return err 696 } 697 defer f.Close() 698 // Write the contents, ignore the number of written bytes 699 _, err = f.WriteString(fmt.Sprintf("%d\n", highScore)) 700 return err 701 } 702 703 // loadHighScore will load the current high score from the highScoreFile, 704 // if possible. 705 func loadHighScore() (uint, error) { 706 data, err := os.ReadFile(highScoreFile) 707 if err != nil { 708 return 0, err 709 } 710 highScoreString := string(bytes.TrimSpace(data)) 711 highScore, err := strconv.ParseUint(highScoreString, 10, 64) 712 if err != nil { 713 return 0, err 714 } 715 return uint(highScore), nil 716 } 717 718 // Game starts the game and returns true if ctrl-q was pressed 719 func Game() (bool, error) { 720 retry: 721 if envNoColor { 722 bobColor = vt100.White 723 bobWonColor = vt100.LightGray 724 bobLostColor = vt100.DarkGray 725 evilGobblerColor = vt100.White 726 gobblerColor = vt100.LightGray 727 gobblerDeadColor = vt100.DarkGray 728 bubbleColor = vt100.DarkGray 729 pelletColor1 = vt100.White 730 pelletColor2 = vt100.White 731 statusTextColor = vt100.Black 732 statusTextBackground = vt100.LightGray 733 resizeColor = vt100.White 734 gameBackgroundColor = vt100.DefaultBackground 735 } else { 736 statusTextBackground = vt100.Blue 737 bobColor = vt100.LightYellow 738 } 739 740 // Try loading the highscore from the file, but ignore any errors 741 highScore, _ := loadHighScore() 742 743 c := vt100.NewCanvas() 744 c.FillBackground(gameBackgroundColor) 745 746 tty, err := vt100.NewTTY() 747 if err != nil { 748 fmt.Fprintln(os.Stderr, "error: "+err.Error()) 749 quitMut.Lock() 750 defer quitMut.Unlock() 751 os.Exit(1) 752 } 753 defer tty.Close() 754 755 tty.SetTimeout(2 * time.Millisecond) 756 757 var ( 758 sigChan = make(chan os.Signal, 1) 759 startingWidth = int(c.W()) 760 bob = NewBob(c, startingWidth) 761 evilGobbler = NewEvilGobbler(c, startingWidth) 762 gobblers = NewGobblers(c, startingWidth, 25) 763 pellets = make([]*Pellet, 0) 764 bubbles = NewBubbles(c, startingWidth, 15) 765 score = uint(0) 766 ) 767 768 signal.Notify(sigChan, syscall.SIGWINCH) 769 go func() { 770 for range sigChan { 771 resizeMut.Lock() 772 // Create a new canvas, with the new size 773 nc := c.Resized() 774 if nc != nil { 775 c.Clear() 776 vt100.Clear() 777 c.Draw() 778 c = nc 779 } 780 781 // Inform all elements that the terminal was resized 782 // TODO: Use a slice of interfaces that can contain all elements 783 for _, pellet := range pellets { 784 pellet.Resize(c) 785 } 786 for _, bubble := range bubbles { 787 bubble.Resize(c) 788 } 789 for _, gobbler := range gobblers { 790 gobbler.Resize(c) 791 } 792 bob.Resize(c) 793 evilGobbler.Resize(c) 794 resizeMut.Unlock() 795 } 796 }() 797 798 vt100.Init() 799 vt100.EchoOff() 800 defer vt100.Close() 801 802 // The loop time that is aimed for 803 var ( 804 loopDuration = time.Millisecond * 10 805 start = time.Now() 806 running = true 807 paused bool 808 statusText string 809 key int 810 gobblersAlive int 811 ) 812 813 // Don't output keypress terminal codes on the screen 814 tty.NoBlock() 815 816 for running { 817 818 // Draw elements in their new positions 819 c.Clear() 820 // c.Draw() 821 822 resizeMut.RLock() 823 for _, pellet := range pellets { 824 pellet.Draw(c) 825 } 826 for _, bubble := range bubbles { 827 bubble.Draw(c) 828 } 829 evilGobbler.Draw(c) 830 for _, gobbler := range gobblers { 831 gobbler.Draw(c) 832 } 833 bob.Draw(c) 834 centerStatus := "Feed the gobblers" 835 rightStatus := fmt.Sprintf("%d alive", gobblersAlive) 836 statusLineLength := int(c.W()) 837 statusLine := " " + statusText 838 839 if !paused && statusLineLength-(len(" "+statusText)+len(rightStatus+" ")) > (len(rightStatus+" ")+len(centerStatus)) { 840 paddingLength := statusLineLength - (len(" "+statusText) + len(centerStatus) + len(rightStatus+" ")) 841 centerLeftLength := int(math.Floor(float64(paddingLength) / 2.0)) 842 centerRightLength := int(math.Ceil(float64(paddingLength) / 2.0)) 843 statusLine += strings.Repeat(" ", centerLeftLength) // padding left of center 844 statusLine += centerStatus 845 statusLine += strings.Repeat(" ", centerRightLength) // padding right of center 846 statusLine += rightStatus + " " 847 } else if statusLineLength-len(" "+statusText) > len(rightStatus+" ") { 848 paddingLength := statusLineLength - (len(" "+statusText) + len(rightStatus+" ")) 849 statusLine += strings.Repeat(" ", paddingLength) // center padding 850 statusLine += rightStatus + " " 851 } else { 852 paddingLength := statusLineLength - len(" "+statusText) 853 statusLine += strings.Repeat("-", paddingLength) 854 } 855 856 c.Write(0, 0, statusTextColor, statusTextBackground, statusLine) 857 resizeMut.RUnlock() 858 859 // vt100.Clear() 860 861 // Update the canvas 862 c.Draw() 863 864 // Wait a bit 865 end := time.Now() 866 passed := end.Sub(start) 867 if passed < loopDuration { 868 remaining := loopDuration - passed 869 time.Sleep(remaining) 870 } 871 start = time.Now() 872 873 // Has the player moved? 874 moved := false 875 876 // Handle events 877 key = tty.Key() 878 switch key { 879 case 253, 119: // Up or w 880 resizeMut.Lock() 881 moved = bob.Up() 882 resizeMut.Unlock() 883 case 255, 115: // Down or s 884 resizeMut.Lock() 885 moved = bob.Down(c) 886 resizeMut.Unlock() 887 case 254, 100: // Right or d 888 resizeMut.Lock() 889 moved = bob.Right() 890 resizeMut.Unlock() 891 case 252, 97: // Left or a 892 resizeMut.Lock() 893 moved = bob.Left() 894 resizeMut.Unlock() 895 case 114: // r 896 goto retry 897 case 113: // q 898 dx := 1 899 dy := 1 900 // Fire eight new pellets 901 pellets = append(pellets, NewPellet(c, bob.x+dx, bob.y+dx, dx, dy)) 902 pellets = append(pellets, NewPellet(c, bob.x-dx, bob.y+dy, -dx, dy)) 903 pellets = append(pellets, NewPellet(c, bob.x+dx, bob.y-dy, dx, -dy)) 904 pellets = append(pellets, NewPellet(c, bob.x-dx, bob.y-dy, -dx, -dy)) 905 pellets = append(pellets, NewPellet(c, bob.x+dx, bob.y, dx, 0)) 906 pellets = append(pellets, NewPellet(c, bob.x-dx, bob.y, -dx, 0)) 907 pellets = append(pellets, NewPellet(c, bob.x, bob.y-dy, 0, -dy)) 908 pellets = append(pellets, NewPellet(c, bob.x, bob.y-dy, 0, -dy)) 909 case 27: // ESC 910 running = false 911 case 17: // ctrl-q 912 return true, nil 913 914 case 19: // ctrl-s 915 // Save a screenshot 916 // Use c.ToImage to generate the image 917 originalImg, err := c.ToImage() 918 if err != nil { 919 statusText = "error: " + err.Error() 920 break 921 } 922 // Create a new image without the first 8 rows 923 bounds := originalImg.Bounds() 924 newImg := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()-8)) 925 draw.Draw(newImg, newImg.Bounds(), originalImg, image.Point{0, 8}, draw.Src) 926 // Create the file 927 screenshotFilename := files.TimestampedFilename("orbiton.png") 928 f, err := os.Create(screenshotFilename) 929 if err != nil { 930 statusText = "error: " + err.Error() 931 break 932 } 933 defer f.Close() 934 // Encode and save the image 935 err = png.Encode(f, newImg) 936 if err != nil { 937 statusText = "error: " + err.Error() 938 break 939 } 940 // Done 941 statusText = "Wrote " + screenshotFilename 942 case 32: // Space 943 if !paused { 944 // Fire a new pellet 945 pellets = append(pellets, NewPellet(c, bob.x, bob.y, bob.x-bob.oldx, bob.y-bob.oldy)) 946 } else { 947 // Progress the pellets, just for entertainment 948 for _, pellet := range pellets { 949 pellet.Next(c, evilGobbler) 950 } 951 } 952 } 953 954 if !paused { 955 // Change state 956 resizeMut.Lock() 957 for _, pellet := range pellets { 958 pellet.Next(c, evilGobbler) 959 } 960 for _, bubble := range bubbles { 961 bubble.Next(c, bob, &gobblers) 962 } 963 for _, gobbler := range gobblers { 964 gobbler.Next(&pellets, bob) 965 } 966 evilGobbler.Next(c, &gobblers) 967 if moved { 968 bob.ToggleState() 969 } 970 resizeMut.Unlock() 971 } 972 // Erase all previous positions not occupied by current items 973 c.Plot(uint(bob.oldx), uint(bob.oldy), ' ') 974 c.Plot(uint(evilGobbler.oldx), uint(evilGobbler.oldy), ' ') 975 for _, pellet := range pellets { 976 c.Plot(uint(pellet.oldx), uint(pellet.oldy), ' ') 977 } 978 for _, bubble := range bubbles { 979 c.Plot(uint(bubble.oldx), uint(bubble.oldy), ' ') 980 } 981 for _, gobbler := range gobblers { 982 c.Plot(uint(gobbler.oldx), uint(gobbler.oldy), ' ') 983 } 984 985 // Clean up removed pellets 986 filteredPellets := make([]*Pellet, 0, len(pellets)) 987 for _, pellet := range pellets { 988 if !pellet.removed { 989 filteredPellets = append(filteredPellets, pellet) 990 } else { 991 c.Plot(uint(pellet.x), uint(pellet.y), ' ') 992 } 993 } 994 pellets = filteredPellets 995 996 if !paused { 997 998 gobblersAlive = 0 999 for _, gobbler := range gobblers { 1000 score += gobbler.counter 1001 gobbler.counter = 0 1002 if !gobbler.dead { 1003 gobblersAlive++ 1004 } else { 1005 gobbler.state = gobblerDeadRune 1006 gobbler.color = gobblerDeadColor 1007 } 1008 } 1009 if gobblersAlive > 0 { 1010 statusText = fmt.Sprintf("Score: %d", score) 1011 } else if gobblersAlive > 0 && evilGobbler.shot { 1012 paused = true 1013 statusText = "You won!" 1014 1015 // The player can still move around bob 1016 bob.state = bobWonRune 1017 1018 if !envNoColor { 1019 bob.color = bobWonColor 1020 statusTextBackground = bobWonColor 1021 } 1022 1023 if score > highScore { 1024 statusText = fmt.Sprintf("You won! New highscore: %d", score) 1025 saveHighScore(score) 1026 } else if score > 0 { 1027 statusText = fmt.Sprintf("You won! Score: %d", score) 1028 } 1029 } else { 1030 paused = true 1031 statusText = "Game over" 1032 1033 // The player can still move around bob 1034 bob.state = bobLostRune 1035 1036 if !envNoColor { 1037 bob.color = bobLostColor 1038 statusTextBackground = bobLostColor 1039 } 1040 1041 if score > highScore { 1042 statusText = fmt.Sprintf("Game over! New highscore: %d - press r to retry - press ctrl-s to save a screenshot", score) 1043 saveHighScore(score) 1044 } else if score > 0 { 1045 statusText = fmt.Sprintf("Game over! Score: %d - press r to retry", score) 1046 } 1047 } 1048 } 1049 } 1050 return false, nil 1051 }