Select Git revision
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)
}