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  }