aboutsummaryrefslogblamecommitdiffstats
path: root/game.go
blob: 3b85a7988934575f1450ba5e9fc6338cc66fd157 (plain) (tree)
1
2
3
4
5
6
7
8
9


            
               
               
             
            
                   
              
                 
              



                                                  



                                        
                                






                                                        
                                                







                                                
                                
                                                   
                                              

                                       


                                                  


                                                       
                                                                                                                      



                                                          
                                                     
                                           





                         

                 

 



                                               










                                


                                          
                                                     
 










                                          
 

                                     

                                      

                                       
                                     
 





                                       



                                                         
 


                                                             

                                        

                                         
                                       

 


                                                  

                                

                                 
                               

 




                                
                    

                  
                        
                           

                                 
                                 
                           
 

                                      

                     

 
                                                                                


                                                       
                                        
 


                                                       


                                                                  
 
                       
                             
                                      
                                          
                                      
                                 
                                                     


         




                                                                   








                                                                                       
                             


                                      

                               
                       


                               


                                       

                                 
                       


                               


                                    

                                 
                       


                                


                                      

                                  
                       

 




                                                 

                                                        









                                                        















                                                       
                                                                                            




























                                                                                             

                                                           

 













                                                                                  










                                 


                       





                  



                                            



                  
 



                      

                      





























                                                                                 











                                           







                                                                      
 





















                                                                             

                         











                                                                
                                 


                                                        
                 
           
 
 

                        

                               
 

                          
                 


                                                 
                   
                                                
                                            
                                 
         













                                                  
                                                                                  





                                                                    
                                                            
                                                             
                                                                                 


                                                             
                                                           



                  

                                               

                                             
                                                 




                      

                           





                              




                       
                                                  
                                               


                                      
                                                                                   
                                                                       
                                                  
                                 
                                                           

         
                                             
                                                                                     
                                           

                                                                                    

                                            

                                                                                 


                              



                                                                                    
 

                                  
                                                                          

                                 
                            




                                                             







                                                                                          































                                                                                           
                          
                                                                                         
                                           
                                                                                      


                 







                                                                              
                                               





                                                  
                                                                  
                                         
                                                                    





                                               

                                                                 
                                 
                                                                                           
 
                                                                 





                                                                                                 
                                                              
                                                      
                                                                              
                                                                                                        
                 


                                             
                                                                            

                                                    
                                                          

                 

                                                                            
                                                  
                 

         
                              

 







                                  



















                                                                        



                                   
                      






















                                                                                          
                                                




                                              


                    


                                                                              

                                                                                      
                                                 

                             
                                                  

                                       
                                                           


                                                          
 




                                                          
                                                              
                                 

                                                                             
                                                                          
                 
 






                                                                                                

                                                                                            
                                                        


                 
                                                                           

                                                                                           

                                                                                           
                                                                          
                 


         

                                    
 


                                      

                                       
                      





                                       



                                          
                     
                     
 


                            

 


                                                           
                                                  
                                                 



                 
                                                                               




                                                               


                                                          
                                                          

 






                                                
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"math/rand"
	"sort"
	"strings"
	"time"

	"github.com/dustinkirkland/golang-petname"
	"github.com/fatih/color"
	"golang.org/x/crypto/ssh"
)

type Hub struct {
	Sessions   map[*Session]struct{}
	Redraw     chan struct{}
	Register   chan *Session
	Unregister chan *Session
}

func NewHub() Hub {
	return Hub{
		Sessions:   make(map[*Session]struct{}),
		Redraw:     make(chan struct{}),
		Register:   make(chan *Session),
		Unregister: make(chan *Session),
	}
}

func (h *Hub) Run(g *Game) {
	for {
		select {
		case <-h.Redraw:
			for s := range h.Sessions {
				go g.Render(s)
			}
		case s := <-h.Register:
			// Hide the cursor
			fmt.Fprint(s, "\033[?25l")

			h.Sessions[s] = struct{}{}
		case s := <-h.Unregister:
			if _, ok := h.Sessions[s]; ok {
				fmt.Fprint(s, "\r\n\r\n~ End of Line ~ \r\n\r\nRemember to use WASD to move!\r\n\r\n")

				// Unhide the cursor
				fmt.Fprint(s, "\033[?25h")

				delete(h.Sessions, s)
				s.c.Close()
			}
		}
	}
}

type Position struct {
	X float64
	Y float64
}

func PositionFromInt(x, y int) Position {
	return Position{float64(x), float64(y)}
}

func (p Position) RoundX() int {
	return int(p.X + 0.5)
}

func (p Position) RoundY() int {
	return int(p.Y + 0.5)
}

type PlayerDirection int

const (
	verticalPlayerSpeed        = 0.007
	horizontalPlayerSpeed      = 0.01
	playerCountScoreMultiplier = 1.25
	playerTimeout              = 15 * time.Second

	playerUpRune    = '⇡'
	playerLeftRune  = '⇠'
	playerDownRune  = '⇣'
	playerRightRune = '⇢'

	playerTrailHorizontal      = '┄'
	playerTrailVertical        = '┆'
	playerTrailLeftCornerUp    = '╭'
	playerTrailLeftCornerDown  = '╰'
	playerTrailRightCornerDown = '╯'
	playerTrailRightCornerUp   = '╮'

	playerRed     = color.FgRed
	playerGreen   = color.FgGreen
	playerYellow  = color.FgYellow
	playerBlue    = color.FgBlue
	playerMagenta = color.FgMagenta
	playerCyan    = color.FgCyan
	playerWhite   = color.FgWhite

	PlayerUp PlayerDirection = iota
	PlayerLeft
	PlayerDown
	PlayerRight
)

var playerColors = []color.Attribute{
	playerRed, playerGreen, playerYellow, playerBlue,
	playerMagenta, playerCyan, playerWhite,
}

var playerBorderColors = map[color.Attribute]color.Attribute{
	playerRed:     color.FgHiRed,
	playerGreen:   color.FgHiGreen,
	playerYellow:  color.FgHiYellow,
	playerBlue:    color.FgHiBlue,
	playerMagenta: color.FgHiMagenta,
	playerCyan:    color.FgHiCyan,
	playerWhite:   color.FgHiWhite,
}

var playerColorNames = map[color.Attribute]string{
	playerRed:     "Red",
	playerGreen:   "Green",
	playerYellow:  "Yellow",
	playerBlue:    "Blue",
	playerMagenta: "Magenta",
	playerCyan:    "Cyan",
	playerWhite:   "White",
}

type PlayerTrailSegment struct {
	Marker rune
	Pos    Position
}

type Player struct {
	s *Session

	Name      string
	CreatedAt time.Time
	Direction PlayerDirection
	Marker    rune
	Color     color.Attribute
	Pos       *Position

	Trail     []PlayerTrailSegment
	WinStreak []color.Attribute

	score float64
}

// NewPlayer creates a new player. If color is below 1, a random color is chosen
func NewPlayer(s *Session, worldWidth, worldHeight int,
	color color.Attribute) *Player {

	rand.Seed(time.Now().UnixNano())

	startX := rand.Float64() * float64(worldWidth)
	startY := rand.Float64() * float64(worldHeight)

	if color < 0 {
		color = playerColors[rand.Intn(len(playerColors))]
	}

	return &Player{
		s:         s,
		CreatedAt: time.Now(),
		Marker:    playerDownRune,
		Direction: PlayerDown,
		Color:     color,
		Pos:       &Position{startX, startY},
	}
}

func (p *Player) addTrailSegment(pos Position, marker rune) {
	segment := PlayerTrailSegment{marker, pos}
	p.Trail = append([]PlayerTrailSegment{segment}, p.Trail...)
}

func (p *Player) calculateScore(delta float64, playerCount int) float64 {
	rawIncrement := (delta * (float64(playerCount-1) * playerCountScoreMultiplier))

	// Convert millisecond increment to seconds
	actualIncrement := rawIncrement / 1000

	return p.score + actualIncrement
}

func (p *Player) HandleUp() {
	if p.Direction == PlayerDown {
		return
	}
	p.Direction = PlayerUp
	p.Marker = playerUpRune
	p.s.didAction()
}

func (p *Player) HandleLeft() {
	if p.Direction == PlayerRight {
		return
	}
	p.Direction = PlayerLeft
	p.Marker = playerLeftRune
	p.s.didAction()
}

func (p *Player) HandleDown() {
	if p.Direction == PlayerUp {
		return
	}
	p.Direction = PlayerDown
	p.Marker = playerDownRune
	p.s.didAction()
}

func (p *Player) HandleRight() {
	if p.Direction == PlayerLeft {
		return
	}
	p.Direction = PlayerRight
	p.Marker = playerRightRune
	p.s.didAction()
}

func (p *Player) Score() int {
	return int(p.score)
}

func (p *Player) Update(g *Game, delta float64) {
	startX, startY := p.Pos.RoundX(), p.Pos.RoundY()

	switch p.Direction {
	case PlayerUp:
		p.Pos.Y -= verticalPlayerSpeed * delta
	case PlayerLeft:
		p.Pos.X -= horizontalPlayerSpeed * delta
	case PlayerDown:
		p.Pos.Y += verticalPlayerSpeed * delta
	case PlayerRight:
		p.Pos.X += horizontalPlayerSpeed * delta
	}

	endX, endY := p.Pos.RoundX(), p.Pos.RoundY()

	// If we moved, add a trail segment.
	if endX != startX || endY != startY {
		var lastSeg *PlayerTrailSegment
		var lastSegX, lastSegY int
		if len(p.Trail) > 0 {
			lastSeg = &p.Trail[0]
			lastSegX = lastSeg.Pos.RoundX()
			lastSegY = lastSeg.Pos.RoundY()
		}

		pos := PositionFromInt(startX, startY)

		switch {
		// Handle corners. This took an ungodly amount of time to figure out. Highly
		// recommend you don't touch.
		case lastSeg != nil &&
			(p.Direction == PlayerRight && endX > lastSegX && endY < lastSegY) ||
			(p.Direction == PlayerDown && endX < lastSegX && endY > lastSegY):
			p.addTrailSegment(pos, playerTrailLeftCornerUp)
		case lastSeg != nil &&
			(p.Direction == PlayerUp && endX > lastSegX && endY < lastSegY) ||
			(p.Direction == PlayerLeft && endX < lastSegX && endY > lastSegY):
			p.addTrailSegment(pos, playerTrailRightCornerDown)
		case lastSeg != nil &&
			(p.Direction == PlayerDown && endX > lastSegX && endY > lastSegY) ||
			(p.Direction == PlayerLeft && endX < lastSegX && endY < lastSegY):
			p.addTrailSegment(pos, playerTrailRightCornerUp)
		case lastSeg != nil &&
			(p.Direction == PlayerRight && endX > lastSegX && endY > lastSegY) ||
			(p.Direction == PlayerUp && endX < lastSegX && endY < lastSegY):
			p.addTrailSegment(pos, playerTrailLeftCornerDown)

		// Vertical and horizontal trails
		case endX == startX && endY < startY:
			p.addTrailSegment(pos, playerTrailVertical)
		case endX < startX && endY == startY:
			p.addTrailSegment(pos, playerTrailHorizontal)
		case endX == startX && endY > startY:
			p.addTrailSegment(pos, playerTrailVertical)
		case endX > startX && endY == startY:
			p.addTrailSegment(pos, playerTrailHorizontal)
		}
	}

	p.score = p.calculateScore(delta, len(g.players()))
}

type ByColor []*Player

func (slice ByColor) Len() int {
	return len(slice)
}

func (slice ByColor) Less(i, j int) bool {
	return playerColorNames[slice[i].Color] < playerColorNames[slice[j].Color]
}

func (slice ByColor) Swap(i, j int) {
	slice[i], slice[j] = slice[j], slice[i]
}

type TileType int

const (
	TileGrass TileType = iota
	TileBlocker
)

type Tile struct {
	Type TileType
}

const (
	gameWidth  = 78
	gameHeight = 22

	keyW = 'w'
	keyA = 'a'
	keyS = 's'
	keyD = 'd'

	keyZ = 'z'
	keyQ = 'q'
	// keyS and keyD are already defined

	keyH = 'h'
	keyJ = 'j'
	keyK = 'k'
	keyL = 'l'

	keyComma = ','
	keyO     = 'o'
	keyE     = 'e'

	keyCtrlC  = 3
	keyEscape = 27
)

type GameManager struct {
	Games         map[string]*Game
	HandleChannel chan ssh.Channel
}

func NewGameManager() *GameManager {
	return &GameManager{
		Games:         map[string]*Game{},
		HandleChannel: make(chan ssh.Channel),
	}
}

// getGameWithAvailability returns a reference to a game with available spots for
// players. If one does not exist, nil is returned.
func (gm *GameManager) getGameWithAvailability() *Game {
	var g *Game

	for _, game := range gm.Games {
		spots := game.AvailableColors()
		if len(spots) > 0 {
			g = game
			break
		}
	}

	return g
}

func (gm *GameManager) SessionCount() int {
	sum := 0
	for _, game := range gm.Games {
		sum += game.SessionCount()
	}
	return sum
}

func (gm *GameManager) GameCount() int {
	return len(gm.Games)
}

func (gm *GameManager) HandleNewChannel(c ssh.Channel, color string) {
	g := gm.getGameWithAvailability()
	if g == nil {
		g = NewGame(gameWidth, gameHeight)
		gm.Games[g.Name] = g

		go g.Run()
	}

	colorOptions := g.AvailableColors()
	finalColor := colorOptions[0]

	// choose the requested color if available
	color = strings.ToLower(color)
	for _, clr := range colorOptions {
		if strings.ToLower(playerColorNames[clr]) == color {
			finalColor = clr
			break
		}
	}

	session := NewSession(c, g.WorldWidth(), g.WorldHeight(), finalColor)
	g.AddSession(session)

	go func() {
		reader := bufio.NewReader(c)
		for {
			r, _, err := reader.ReadRune()
			if err != nil {
				fmt.Println(err)
				break
			}

			switch r {
			case keyW, keyZ, keyK, keyComma:
				session.Player.HandleUp()
			case keyA, keyQ, keyH:
				session.Player.HandleLeft()
			case keyS, keyJ, keyO:
				session.Player.HandleDown()
			case keyD, keyL, keyE:
				session.Player.HandleRight()
			case keyCtrlC, keyEscape:
				if g.SessionCount() == 1 {
					delete(gm.Games, g.Name)
				}

				g.RemoveSession(session)
			}
		}
	}()
}

type Game struct {
	Name      string
	Redraw    chan struct{}
	HighScore int

	// Top left is 0,0
	level [][]Tile
	hub   Hub
}

func NewGame(worldWidth, worldHeight int) *Game {
	g := &Game{
		Name:   petname.Generate(1, ""),
		Redraw: make(chan struct{}),
		hub:    NewHub(),
	}
	g.initalizeLevel(worldWidth, worldHeight)

	return g
}

func (g *Game) initalizeLevel(width, height int) {
	g.level = make([][]Tile, width)
	for x := range g.level {
		g.level[x] = make([]Tile, height)
	}

	// Default world to grass
	for x := range g.level {
		for y := range g.level[x] {
			g.setTileType(Position{float64(x), float64(y)}, TileGrass)
		}
	}
}

func (g *Game) setTileType(pos Position, tileType TileType) error {
	outOfBoundsErr := "The given %s value (%s) is out of bounds"
	if pos.RoundX() > len(g.level) || pos.RoundX() < 0 {
		return fmt.Errorf(outOfBoundsErr, "X", pos.X)
	} else if pos.RoundY() > len(g.level[pos.RoundX()]) || pos.RoundY() < 0 {
		return fmt.Errorf(outOfBoundsErr, "Y", pos.Y)
	}

	g.level[pos.RoundX()][pos.RoundY()].Type = tileType

	return nil
}

func (g *Game) players() map[*Player]*Session {
	players := make(map[*Player]*Session)

	for session := range g.hub.Sessions {
		players[session.Player] = session
	}

	return players
}

// Characters for rendering
const (
	verticalWall   = '║'
	horizontalWall = '═'
	topLeft        = '╔'
	topRight       = '╗'
	bottomRight    = '╝'
	bottomLeft     = '╚'

	grass   = ' '
	blocker = '■'
)

// Warning: this will only work with square worlds
func (g *Game) worldString(s *Session) string {
	worldWidth := len(g.level)
	worldHeight := len(g.level[0])

	// Create two dimensional slice of strings to represent the world. It's two
	// characters larger in each direction to accomodate for walls.
	strWorld := make([][]string, worldWidth+2)
	for x := range strWorld {
		strWorld[x] = make([]string, worldHeight+2)
	}

	// Load the walls into the rune slice
	borderColorizer := color.New(playerBorderColors[s.Player.Color]).SprintFunc()
	for x := 0; x < worldWidth+2; x++ {
		strWorld[x][0] = borderColorizer(string(horizontalWall))
		strWorld[x][worldHeight+1] = borderColorizer(string(horizontalWall))
	}
	for y := 0; y < worldHeight+2; y++ {
		strWorld[0][y] = borderColorizer(string(verticalWall))
		strWorld[worldWidth+1][y] = borderColorizer(string(verticalWall))
	}

	// Time for the edges!
	strWorld[0][0] = borderColorizer(string(topLeft))
	strWorld[worldWidth+1][0] = borderColorizer(string(topRight))
	strWorld[worldWidth+1][worldHeight+1] = borderColorizer(string(bottomRight))
	strWorld[0][worldHeight+1] = borderColorizer(string(bottomLeft))

	// Draw the player's score
	scoreStr := fmt.Sprintf(
		" Score: %d : Your High Score: %d : Game High Score: %d ",
		s.Player.Score(),
		s.HighScore,
		g.HighScore,
	)
	for i, r := range scoreStr {
		strWorld[3+i][0] = borderColorizer(string(r))
	}

	// Draw the player's color
	colorStr := fmt.Sprintf(" %s ", playerColorNames[s.Player.Color])
	colorStrColorizer := color.New(s.Player.Color).SprintFunc()
	for i, r := range colorStr {
		charsRemaining := len(colorStr) - i
		strWorld[len(strWorld)-3-charsRemaining][0] = colorStrColorizer(string(r))
	}

	// Draw everyone's scores
	if len(g.players()) > 1 {
		// Sort the players by color name
		players := []*Player{}

		for player := range g.players() {
			if player == s.Player {
				continue
			}

			players = append(players, player)
		}

		sort.Sort(ByColor(players))
		startX := 3

		// Actually draw their scores
		for _, player := range players {
			colorizer := color.New(player.Color).SprintFunc()
			scoreStr := fmt.Sprintf(" %s: %d",
				playerColorNames[player.Color],
				player.Score(),
			)
			for _, r := range scoreStr {
				strWorld[startX][len(strWorld[0])-1] = colorizer(string(r))
				startX++
			}
		}

		// Add final spacing next to wall
		strWorld[startX][len(strWorld[0])-1] = " "
	} else {
		warning :=
			" Warning: Other Players Must be in This Game for You to Score! "
		for i, r := range warning {
			strWorld[3+i][len(strWorld[0])-1] = borderColorizer(string(r))
		}
	}

	// Draw the game's name
	nameStr := fmt.Sprintf(" %s ", g.Name)
	for i, r := range nameStr {
		charsRemaining := len(nameStr) - i
		strWorld[len(strWorld)-3-charsRemaining][len(strWorld[0])-1] =
			borderColorizer(string(r))
	}

	// Load the level into the string slice
	for x := 0; x < worldWidth; x++ {
		for y := 0; y < worldHeight; y++ {
			tile := g.level[x][y]

			switch tile.Type {
			case TileGrass:
				strWorld[x+1][y+1] = string(grass)
			case TileBlocker:
				strWorld[x+1][y+1] = string(blocker)
			}
		}
	}

	// Load the players into the rune slice
	for player := range g.players() {
		colorizer := color.New(player.Color).SprintFunc()

		pos := player.Pos
		strWorld[pos.RoundX()+1][pos.RoundY()+1] = colorizer(string(player.Marker))

                // Make the rainbow of trail colors to cycle over
		trailColorizers := make([](func(...interface{}) string), len(player.WinStreak)+1)
		trailColorizers[0] = colorizer
		for i, winColor := range player.WinStreak {
			trailColorizers[i+1] = color.New(winColor).SprintFunc()
		}

		// Load the player's trail into the rune slice
		for i, segment := range player.Trail {
			x, y := segment.Pos.RoundX()+1, segment.Pos.RoundY()+1
			strWorld[x][y] = trailColorizers[i%len(trailColorizers)](string(segment.Marker))
		}
	}

	// Convert the rune slice to a string
	buffer := bytes.NewBuffer(make([]byte, 0, worldWidth*worldHeight*2))
	for y := 0; y < len(strWorld[0]); y++ {
		for x := 0; x < len(strWorld); x++ {
			buffer.WriteString(strWorld[x][y])
		}

		// Don't add an extra newline if we're on the last iteration
		if y != len(strWorld[0])-1 {
			buffer.WriteString("\r\n")
		}
	}

	return buffer.String()
}

func (g *Game) WorldWidth() int {
	return len(g.level)
}

func (g *Game) WorldHeight() int {
	return len(g.level[0])
}

func (g *Game) AvailableColors() []color.Attribute {
	usedColors := map[color.Attribute]bool{}
	for _, color := range playerColors {
		usedColors[color] = false
	}

	for player := range g.players() {
		usedColors[player.Color] = true
	}

	availableColors := []color.Attribute{}
	for color, used := range usedColors {
		if !used {
			availableColors = append(availableColors, color)
		}
	}

	return availableColors
}

func (g *Game) SessionCount() int {
	return len(g.hub.Sessions)
}

func (g *Game) Run() {
	// Proxy g.Redraw's channel to g.hub.Redraw
	go func() {
		for {
			g.hub.Redraw <- <-g.Redraw
		}
	}()

	// Run game loop
	go func() {
		var lastUpdate time.Time

		c := time.Tick(time.Second / 60)
		for now := range c {
			g.Update(float64(now.Sub(lastUpdate)) / float64(time.Millisecond))

			lastUpdate = now
		}
	}()

	// Redraw regularly.
	//
	// TODO: Implement diffing and only redraw when needed
	go func() {
		c := time.Tick(time.Second / 10)
		for range c {
			g.Redraw <- struct{}{}
		}
	}()

	g.hub.Run(g)
}

// Update is the main game logic loop. Delta is the time since the last update
// in milliseconds.
func (g *Game) Update(delta float64) {
	// We'll use this to make a set of all of the coordinates that are occupied by
	// trails
	trailCoordMap := make(map[string]*Player)

	// Update player data
	for player, session := range g.players() {
		player.Update(g, delta)

		// Update session high score, if applicable
		if player.Score() > session.HighScore {
			session.HighScore = player.Score()
		}

		// Update global high score, if applicable
		if player.Score() > g.HighScore {
			g.HighScore = player.Score()
		}

		// Restart the player if they're out of bounds
		pos := player.Pos
		if pos.RoundX() < 0 || pos.RoundX() >= len(g.level) ||
			pos.RoundY() < 0 || pos.RoundY() >= len(g.level[0]) {
			session.StartOver(g.WorldWidth(), g.WorldHeight())
		}

		// Kick the player if they've timed out
		if time.Now().Sub(session.LastAction) > playerTimeout {
			fmt.Fprint(session, "\r\n\r\nYou were terminated due to inactivity\r\n")
			g.RemoveSession(session)
			return
		}

		for _, seg := range player.Trail {
			coordStr := fmt.Sprintf("%d,%d", seg.Pos.RoundX(), seg.Pos.RoundY())
			trailCoordMap[coordStr] = player
		}
	}

	// Check if any players collide with a trail and restart them if so
	for player, session := range g.players() {
		playerPos := fmt.Sprintf("%d,%d", player.Pos.RoundX(), player.Pos.RoundY())
		if otherPlayer, collided := trailCoordMap[playerPos]; collided {
			otherPlayer.WinStreak = append(otherPlayer.WinStreak, player.Color)
			session.StartOver(g.WorldWidth(), g.WorldHeight())
		}
	}
}

func (g *Game) Render(s *Session) {
	worldStr := g.worldString(s)

	var b bytes.Buffer
	b.WriteString("\033[H\033[2J")
	b.WriteString(worldStr)

	// Send over the rendered world
	io.Copy(s, &b)
}

func (g *Game) AddSession(s *Session) {
	g.hub.Register <- s
}

func (g *Game) RemoveSession(s *Session) {
	g.hub.Unregister <- s
}

type Session struct {
	c ssh.Channel

	LastAction time.Time
	HighScore  int
	Player     *Player
}

func NewSession(c ssh.Channel, worldWidth, worldHeight int,
	color color.Attribute) *Session {

	s := Session{c: c, LastAction: time.Now()}
	s.newGame(worldWidth, worldHeight, color)

	return &s
}

func (s *Session) newGame(worldWidth, worldHeight int, color color.Attribute) {
	s.Player = NewPlayer(s, worldWidth, worldHeight, color)
}

func (s *Session) didAction() {
	s.LastAction = time.Now()
}

func (s *Session) StartOver(worldWidth, worldHeight int) {
	s.newGame(worldWidth, worldHeight, s.Player.Color)
}

func (s *Session) Read(p []byte) (int, error) {
	return s.c.Read(p)
}

func (s *Session) Write(p []byte) (int, error) {
	return s.c.Write(p)
}
Un proyecto texto-plano.xyz