Skip to content
Snippets Groups Projects
Select Git revision
  • master
  • slidy
  • wobbly
3 results

bot.go

Blame
  • bot.go 7.60 KiB
    package main
    
    import (
    	"encoding/json"
    	"compress/gzip"
    	"io"
    	"net"
    	"time"
    )
    
    // BEGIN Bot API Types
    
    // Connect to plugin port and send two lines, one containing the string
    // 'register' and one containing the string 'bot', to receive updates.
    // To control your snake, send the ascii characters also used by human clients.
    
    // API version used to notify bots trying to use an outdated version
    type BotAPIVersion struct {
    	Major int
    	Minor int
    }
    var (
    	botAPIVersion BotAPIVersion = BotAPIVersion{ 1, 0 }
    )
    
    // Every update is one gzip compressed line containing a json encoded BotAPIUpdate
    // Type indicates which member is not null
    type BotAPIUpdate struct {
    	Type           BotAPIUpdateType
    	BotAPIInit     *BotAPIInit
    	BotAPIBlocks   []BotAPIBlock
    	BotAPISpecials *BotAPISpecials
    	BotAPIAlive    *BotAPIAlive
    	BotAPIBorder   *BotAPIBorder
    }
    
    type BotAPIUpdateType int
    
    // Integer to indicate which member of BotAPIUpdate is valid
    const (
    	botAPIInit BotAPIUpdateType = iota
    	botAPIBlocks
    	botAPISpecials
    	botAPIAlive
    	botAPIBorder
    )
    
    // Player representation used in BotAPIInit update
    type BotApiPlayer struct {
    	Id int
    	Alive bool
    }
    
    // Initial information about the running game
    // This is the first update sent to a newly connected bot
    type BotAPIInit struct {
    	// Size of the board
    	FieldWidth int
    	FieldHeight int
    	// Milliseconds between game ticks
    	UpdateInterval uint
    	// Game mode, see definition of GameMode for possible values
    	GameMode GameMode
    	// Currently connected players including the bot
    	Players []BotApiPlayer
    	// Player id of the bot
    	Self int
    }
    
    // Every tick the bot receives one list of BotAPIBlocks, one
    // for each block that change on the board since the last tick.
    //
    // This is to be used to determine when one game tick is over.
    //
    // When the bot sends a redraw command, it receives a full list
    // of BotAPIBlocks, on for every block on the board. This is sent
    // additionally to (not instead of) the tick based updates.
    //
    // Exactly one of Snake, Food and Portal will be set. This corresponds
    // to the entity that is visible to human players.
    type BotAPIBlock struct {
    	X int
    	Y int
    	Snake *BUSnake
    	Food *BUFood
    	Portal *BUPortal
    }
    
    // This block contains a snake segment belonging to player <Id>.
    type BUSnake struct {
    	Id int
    }
    
    // This block contains food. <Poison> indicates whether the food is poisoned.
    type BUFood struct {
    	Poison bool
    }
    
    // This block contains a portal. <Identifier> can be used to find out which other
    // portal this one leads to.
    type BUPortal struct {
    	Identifier byte
    }
    
    // The specials of this bot changed. <Specials> is the complete list of this bot's specials.
    // This is sent in each of the following cases:
    //   - The bot gains a special from eating food
    //   - The bot uses or discards a special
    //   - The bot's specials are cleared by Clear Specials
    type BotAPISpecials struct {
    	// See food.go (special*Id) for possible values
    	Specials []int
    }
    
    // This is sent when a player spawns or dies. <Id> is that players id. <Alive> indicates
    // whether he now is alive or dead.
    type BotAPIAlive struct {
    	Id int
    	Alive bool
    }
    
    // The game's wrapMode changed. WrapMode can be used to determine where the bot will
    // move to when running into/through the border of the board.
    //
    // See definition of WrapMode for possible values.
    type BotAPIBorder struct {
    	WrapMode WrapMode
    }
    
    // END Bot API Types
    
    type BotPlugin struct {
    	failed bool
    	writer *gzip.Writer
    	player *Player
    }
    
    // botScreen handles machine readable gamestate rendering
    
    type BotScreen struct {
    	diff [][]bool
    	lastFrameDiff []byte
    }
    
    var (
    	botScreen *BotScreen
    )
    
    func NewBotScreen(h, w int) (s *BotScreen) {
    	s = &BotScreen{}
    	s.lastFrameDiff = make([]byte, 0)
    	s.diff = make([][]bool, w)
    	for x := range s.diff {
    		s.diff[x] = make([]bool, h)
    	}
    	return
    }
    
    func (s *BotScreen) UpdateDiff() {
    	botU := &BotAPIUpdate{Type: botAPIBlocks, BotAPIBlocks: make([]BotAPIBlock, 0)}
    	for x := range s.diff {
    		for y := range s.diff[x] {
    			if s.diff[x][y] {
    				s.diff[x][y] = false
    				bu := BotAPIBlock{X: x, Y: y};
    				i := ground.buf[x][y].info
    				if i.snake != nil {
    					bu.Snake = &BUSnake{i.snake.(*Snake).player.Id}
    				} else if i.food != nil {
    					bu.Food = &BUFood{i.food.(*Food).poisonLevel > 0}
    				} else if i.portal != nil {
    					bu.Portal = &BUPortal{byte('a' + i.portal.(*Portal).mark - 'α')}
    				}
    				botU.BotAPIBlocks = append(botU.BotAPIBlocks, bu)
    			}
    		}
    	}
    	s.lastFrameDiff, _ = json.Marshal(botU)
    	s.lastFrameDiff = append(s.lastFrameDiff, '\n')
    }
    
    func NewBotPlayer(conn net.Conn, name string) *Player {
    	p := &BotPlugin{
    		failed: false,
    		writer: gzip.NewWriter(conn),
    	}
    	player := NewPlayer(conn, p)
    	p.player = player
    	p.player.name = name
    	return p.player
    }
    
    // synchronous intialiasation
    func (p *BotPlugin) Init() {
    	ps := make([]BotApiPlayer, 0)
    	for id, p := range players {
    		ps = append(ps, BotApiPlayer{id, !p.IsSpec()})
    	}
    	botU := BotAPIUpdate{
    		Type: botAPIInit,
    		BotAPIInit: &BotAPIInit{
    			FieldHeight: ground.h,
    			FieldWidth: ground.w,
    			UpdateInterval: gameConfig.UpdateInterval,
    			GameMode: gameMode,
    			Players: ps,
    			Self: p.player.Id,
    		},
    	}
    	p.Send(&botU)
    	p.UpdateBorder()
    }
    
    func (p *BotPlugin) Render() {
    	diff := botScreen.lastFrameDiff
    	// do not use p.Send, since lastFrameDiff is already json encoded
    	_, err := p.writer.Write(diff)
    	if err != nil {
    		p.failed = true
    		return
    	}
    	err = p.writer.Flush()
    	if err != nil {
    		p.failed = true
    	}
    }
    
    func (p *BotPlugin) deregister(message string) {
    	io.WriteString(p.writer, message)
    }
    
    // Bots don't need help
    func (p *BotPlugin) ShowHelp() {
    	return
    }
    
    func (p *BotPlugin) ReadNameFromUser() {
    	p.player.ingame = false
    	go func() {
    		p.player.reader.SetDeadline(time.Now().Add(connectionConfig.DialogTimeout * time.Minute))
    		name, err := p.player.reader.ReadLine()
    		// TODO extract this here and in human into a function
    		// sanitize username
    		name = nameWhitelist.ReplaceAllString(name, "")
    		if len(name) > 5 {
    			name = name[:5]
    		}
    
    		if err != nil {
    			name = ""
    		}
    
    		AddTickEvent(1, nil, func(snakes ...*Snake) {
    			p.player.ingame = true
    			if name != "" {
    				p.player.name = "bot_"+name
    				p.player.UpdatePlayerListEntry()
    			}
    		})
    	}()
    }
    
    func (p *BotPlugin) IsFailed() bool {
    	return p.failed
    }
    
    func (p *BotPlugin) Send(data interface{}) {
    	j, _ := json.Marshal(data)
    	j = append(j, '\n')
    	_, err := p.writer.Write(j)
    	if err != nil {
    		p.failed = true
    		return
    	}
    	err = p.writer.Flush()
    	if err != nil {
    		p.failed = true
    	}
    }
    
    func (p *BotPlugin) InitialUpdate() {
    	botU := BotAPIUpdate{Type: botAPIBlocks, BotAPIBlocks: make([]BotAPIBlock, 0)}
    	for x := range ground.buf {
    		for y := range ground.buf[x] {
    			bu := BotAPIBlock{X: x, Y: y};
    			i := ground.buf[x][y].info
    			if i.snake != nil {
    				bu.Snake = &BUSnake{i.snake.(*Snake).player.Id}
    			} else if i.food != nil {
    				bu.Food = &BUFood{i.food.(*Food).poisonLevel > 0}
    			} else if i.portal != nil {
    				bu.Portal = &BUPortal{byte('a' + i.portal.(*Portal).mark - 'α')}
    			}
    			botU.BotAPIBlocks = append(botU.BotAPIBlocks, bu)
    		}
    	}
    	p.Send(&botU)
    }
    
    // Do not send full updates other than InitialUpdate to bots,
    // they can not get things like drawing artifacts from terminal resizing
    func (p *BotPlugin) UpdateAll() {}
    
    func (p *BotPlugin) UpdateSpecials() {
    	botU := BotAPIUpdate{
    		Type: botAPISpecials,
    		BotAPISpecials: &BotAPISpecials{
    			Specials: p.player.specials,
    		},
    	}
    	p.Send(&botU)
    }
    
    func (p *BotPlugin) UpdateAlive(id int, alive bool) {
    	botU := BotAPIUpdate{
    		Type: botAPIAlive,
    		BotAPIAlive: &BotAPIAlive{
    			Id: id,
    			Alive: alive,
    		},
    	}
    	p.Send(&botU)
    }
    
    func (p *BotPlugin) UpdateBorder() {
    	botU := &BotAPIUpdate{
    		Type: botAPIBorder,
    		BotAPIBorder: &BotAPIBorder{
    			WrapMode: wrapMode,
    		},
    	}
    	p.Send(&botU)
    }