Commit dd9b3d7
Changed files (5)
ai.go
@@ -0,0 +1,15 @@
+package main
+
+type Bot struct {
+ Updates <-chan Update
+ Map
+}
+
+func (ai *Bot) RecieveUpdates() {
+ for {
+ u := <-ai.Updates
+ ai.Map.Patch(u)
+ ai.Map.Print()
+ }
+
+}
generals.go
@@ -0,0 +1,150 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "go.uber.org/zap"
+)
+
+const (
+ GENIO_SETNAME = "error_set_username"
+ GENIO_NAMETAKEN = "This username is already taken."
+ GENIO_QUEUE = "queue_update"
+ GENIO_CHAT = "chat_message"
+ GENIO_PRESTART = "pre_game_start"
+ GENIO_START = "game_start"
+ GENIO_UPDATE = "game_update"
+ GENIO_WIN = "game_won"
+ GENIO_LOSE = "game_lost"
+ GENIO_OVER = "game_over"
+ GENIO_EMPTY = -1
+ GENIO_MOUNTAIN = -2
+ GENIO_FOG = -3
+ GENIO_FOG_OBSTACLE = -4
+)
+
+type Update struct {
+ Scores []PlayerScore `json:"scores"`
+ Turn int `json:"turn"`
+ AttackIndex int `json:"attackIndex"`
+ Generals []int `json:"generals"`
+ MapDiff []int `json:"map_diff"`
+ CitiesDiff []int `json:"cities_diff"`
+}
+
+type PlayerScore struct {
+ PlayerIndex int `json:"i"`
+ Dead bool `json:"dead"`
+ Army int `json:"total"`
+ Land int `json:"tiles"`
+}
+
+type ChatMessage struct {
+ Text string `json:"text"`
+ Username string `json:"username"`
+ Prefix string `json:"prefix"`
+ PlayerIndex int `json:"playerIndex"`
+}
+
+type Game struct {
+ UserIndex int `json:"playerIndex"` // YOU!
+ ReplayID string `json:"replay_id"`
+ ChatID string `json:"chat_room"`
+ TeamChatID string `json:"team_chat_room"`
+ Usernames []string `json:"usernames"`
+ Teams []int `json:"teams"`
+}
+
+func DecodeEvent(p []byte, updates chan<- Update) (err error) {
+ var (
+ m []json.RawMessage
+ t string
+ )
+ dec := json.NewDecoder(bytes.NewBuffer(p))
+ err = dec.Decode(&m)
+ if err != nil {
+ l.Error(zap.Error(err))
+ return err
+ }
+ err = json.Unmarshal(m[0], &t)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+
+ switch t {
+
+ case GENIO_SETNAME:
+ var msg string
+
+ err = json.Unmarshal(m[1], &msg)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+ if msg == GENIO_NAMETAKEN {
+ // This message doesn't really make sense.
+ // You get this "error" even if you are the owner of the name
+ l.Info("Set Name Failed (expected) ", zap.String("msg", msg))
+ } else if msg == "" {
+ l.Info("Set Name Success")
+ }
+
+ case GENIO_CHAT:
+ var room string
+ var chat ChatMessage
+
+ err = json.Unmarshal(m[1], &room)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+ err = json.Unmarshal(m[2], &chat)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+ l.Infof("Chat - %s: %s", chat.Username, chat.Text)
+
+ case GENIO_QUEUE:
+ // TODO if this is important someday
+ l.Info("Queue Update ", string(p))
+
+ case GENIO_PRESTART:
+ l.Info("Game Starting")
+
+ case GENIO_START:
+ var game Game
+
+ err = json.Unmarshal(m[1], &game)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+ l.Info("Game Start ", zap.Any("game", game))
+
+ case GENIO_UPDATE:
+ var u Update
+
+ err = json.Unmarshal(m[1], &u)
+ if err != nil {
+ l.Error(zap.String("type", t), zap.Error(err))
+ return err
+ }
+ updates <- u
+ l.Debug("Update sent to AI ")
+
+ case GENIO_WIN:
+ l.Info("Game Won")
+
+ case GENIO_LOSE:
+ l.Info("Game Lost")
+
+ case GENIO_OVER:
+ l.Info("Game Over")
+
+ default:
+ l.Error("Unhandled Event", zap.String("type", t), zap.String("event", string(p)))
+ }
+ return err
+}
main.go
@@ -1,80 +1,58 @@
package main
import (
- "bytes"
- "encoding/json"
- "github.com/gorilla/websocket"
- "log"
- "time"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+ "os"
+ "os/signal"
)
-type SocketIO struct {
- *websocket.Conn
-}
+var l *zap.SugaredLogger
-func (s *SocketIO) Emit(msg ...interface{}) error {
- w, err := s.NextWriter(websocket.TextMessage)
- if err != nil {
- return err
- }
- _, err = w.Write([]byte("42"))
- j := json.NewEncoder(w)
- return j.Encode(msg)
+func init() {
+ conf := zap.NewDevelopmentConfig()
+ level := zap.NewAtomicLevel()
+ level.SetLevel(zapcore.InfoLevel)
+ conf.Level = level
+ conf.DisableStacktrace = true
+ logger, _ := conf.Build()
+ l = logger.Sugar()
}
-func (s *SocketIO) Ping() error {
- w, err := s.NextWriter(websocket.TextMessage)
- if err != nil {
- return err
+
+func exitHandler() {
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+ for _ = range c {
+ l.Warn("Caught SIGINT, Exiting")
+ return
}
- _, err = w.Write([]byte("2ping"))
- return err
}
func main() {
- generals_url := "ws://botws.generals.io/socket.io/?EIO=3&transport=websocket"
- user_id := "k_00003"
- username := "k_bot3"
+ user_id := "k_00004"
+ username := "[Bot]k_0004"
- dialer := &websocket.Dialer{}
- dialer.EnableCompression = false
+ var ai Bot
+ var g GeneralsIO
+ u := make(chan Update)
+ ai.Updates = u
+ g.Updates = u
+ g.Connect()
- conn, _, err := dialer.Dial(generals_url, nil)
- c := &SocketIO{conn}
+ err := g.Emit("set_username", user_id, username)
if err != nil {
- log.Fatal(err)
+ l.Fatal(err)
}
- err = c.Emit("set_username", user_id, username)
+ err = g.Emit("join_private", "test", user_id)
if err != nil {
- log.Fatal(err)
+ l.Fatal(err)
}
- err = c.Emit("join_private", "test", user_id)
+ err = g.Emit("set_force_start", "test", true)
if err != nil {
- log.Fatal(err)
- }
- go func() {
- for range time.Tick(5 * time.Second) {
- c.Ping()
- }
- }()
- for {
- _, message, err := c.ReadMessage()
- if err != nil {
- log.Fatal(err)
- }
- log.Println("Got: ", string(message))
- dec := json.NewDecoder(bytes.NewBuffer(message))
- var msgType int
- dec.Decode(&msgType)
-
- if msgType == 42 {
- var raw json.RawMessage
- dec.Decode(&raw)
- eventname := ""
- data := []interface{}{&eventname}
- json.Unmarshal(raw, &data)
- //if f, ok := c.events[eventname]; ok {
- // f(raw)
- //}
- }
+ l.Fatal(err)
}
+ go g.Ping(5)
+ go g.RecievePackets() // Packets > Messages > Events
+ go ai.RecieveUpdates()
+ exitHandler()
}
map.go
@@ -0,0 +1,158 @@
+package main
+
+import (
+ "fmt"
+ "github.com/fatih/color"
+)
+
+type Map struct {
+ Scores []PlayerScore
+ Turn int
+ AttackIndex int
+ Generals []int
+ MapArray []int
+ CitiesArray []int
+ Width int
+ Height int
+ Size int
+ Tiles [][]Tile
+}
+
+type Tile struct {
+ Owner int // Player Index
+ Armies int
+ Terrain TerrainType // Terrain Type
+ Seen bool
+ Fog bool
+}
+
+type TerrainType int
+
+const (
+ EMPTY TerrainType = iota + 1
+ MOUNTAIN
+ OBSTACLE
+ CITY
+ CAPITAL // game calls this a GENERAL
+)
+
+func (m *Map) Print() {
+ fmt.Println("TILES", m.Height, m.Width, m.Size)
+ for i := 0; i < m.Height; i++ {
+ fmt.Println()
+ for j := 0; j < m.Width; j++ {
+ m.Tiles[i][j].Print()
+ }
+ }
+ fmt.Println()
+}
+
+func (m *Map) Patch(u Update) {
+ m.MapArray = patch(m.MapArray, u.MapDiff)
+ m.CitiesArray = patch(m.CitiesArray, u.CitiesDiff)
+ m.Generals = u.Generals
+ // Init
+ if m.Tiles == nil {
+ m.Width = m.MapArray[0]
+ m.Height = m.MapArray[1]
+ m.Tiles = make([][]Tile, m.Height)
+ m.Size = m.Width * m.Height
+ for i := 0; i < m.Height; i++ {
+ m.Tiles[i] = make([]Tile, m.Width)
+ }
+ }
+ // Apply patch to Map/Tiles
+ for i := 0; i < m.Height; i++ {
+ for j := 0; j < m.Width; j++ {
+ m.Tiles[i][j].Armies = m.MapArray[(i*m.Width)+j+2]
+ m.Tiles[i][j].DecodeTerrain(m.MapArray[(i*m.Width)+j+2+m.Size])
+ for k := range m.Generals {
+ if i*m.Width+j == m.Generals[k] {
+ m.Tiles[i][j].Owner = k
+ m.Tiles[i][j].Terrain = CAPITAL
+ }
+ }
+ for l := range m.CitiesArray {
+ if i*m.Width+j == m.CitiesArray[l] {
+ m.Tiles[i][j].Terrain = CITY
+ }
+ }
+ }
+ }
+}
+
+func patch(old []int, diff []int) (new []int) {
+ i := 0
+ for i < len(diff) {
+ if diff[i] > 0 {
+ new = append(new, old[len(new):(len(new)+diff[i])]...)
+ }
+ i++
+ if i < len(diff) && diff[i] > 0 {
+ new = append(new, diff[i+1:(i+1+diff[i])]...)
+ i += diff[i]
+ }
+ i++
+ }
+ return new
+}
+
+func (t *Tile) DecodeTerrain(terrain int) {
+ switch terrain {
+ case GENIO_EMPTY:
+ t.Seen = true
+ t.Terrain = EMPTY
+ t.Owner = 0
+ case GENIO_MOUNTAIN:
+ t.Seen = true
+ t.Terrain = MOUNTAIN
+ case GENIO_FOG:
+ t.Fog = true
+ t.Terrain = EMPTY
+ case GENIO_FOG_OBSTACLE:
+ // Retain knowledge.
+ // Obstacle can become a Mountain or City,
+ // not the other way around
+ if t.Terrain == 0 {
+ t.Terrain = OBSTACLE
+ }
+ t.Fog = true
+ default:
+ t.Seen = true
+ t.Owner = terrain
+ }
+}
+
+func (t *Tile) Print() {
+ p := color.New(color.FgWhite)
+ if t.Fog {
+ if t.Seen {
+ p.Add(color.BgHiBlue)
+ } else {
+ p.Add(color.BgHiBlue)
+ }
+ }
+ switch t.Terrain {
+ case EMPTY:
+ if t.Fog || t.Armies == 0 {
+ p.Printf(" _")
+ } else {
+ // TODO if owner != PlayerID of bot = ReD
+ p.Printf("%3d", t.Armies)
+ }
+ case MOUNTAIN:
+ p.Printf(" M")
+ case OBSTACLE:
+ p.Printf(" ?")
+ case CITY:
+ p.Add(color.BgCyan)
+ p.Printf("%3d", t.Armies)
+ case CAPITAL:
+ p.Add(color.Bold, color.BgHiMagenta)
+ p.Printf("%3d", t.Armies)
+ default:
+ p.Add(color.FgRed)
+ p.Printf("%3d", t.Armies)
+ }
+
+}
sockets.go
@@ -0,0 +1,127 @@
+package main
+
+import (
+ "encoding/json"
+ "github.com/gorilla/websocket"
+ "go.uber.org/zap"
+ "strconv"
+ "time"
+)
+
+type GeneralsIO struct {
+ *websocket.Conn
+ Updates chan<- Update
+}
+
+const (
+ ENGINEIO_OPEN = 0
+ ENGINEIO_CLOSE = 1
+ ENGINEIO_PING = 2
+ ENGINEIO_PONG = 3
+ ENGINEIO_MESSAGE = 4
+ SOCKETIO_CONNECT = 0
+ SOCKETIO_EVENT = 2
+)
+
+// Packet > Message > Event
+
+type Packet struct {
+ Type int
+ MessageType int
+ Payload []byte
+}
+
+func (g *GeneralsIO) Connect() (err error) {
+ dialer := &websocket.Dialer{}
+ dialer.EnableCompression = false
+ generals_url := "ws://botws.generals.io/socket.io/?EIO=3&transport=websocket"
+ conn, _, err := dialer.Dial(generals_url, nil)
+ if err != nil {
+ return err
+ }
+ g.Conn = conn
+ return err
+}
+
+func (g *GeneralsIO) Emit(msg ...interface{}) error {
+ w, err := g.NextWriter(websocket.TextMessage)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write([]byte("42"))
+ j := json.NewEncoder(w)
+ return j.Encode(msg)
+}
+
+func (g *GeneralsIO) Ping(t int) {
+ for range time.Tick(time.Duration(t) * time.Second) {
+ l.Debug("Ping")
+ w, err := g.NextWriter(websocket.TextMessage)
+ if err != nil {
+ l.Fatal(err)
+ }
+ _, err = w.Write([]byte("2ping"))
+ if err != nil {
+ l.Fatal(err)
+ }
+ }
+}
+
+func DecodePacket(b []byte) (p Packet, err error) {
+ // TODO: b > 0
+ t, err := strconv.Atoi(string(b[0]))
+ if err != nil {
+ return p, err
+ }
+
+ switch t {
+ case ENGINEIO_OPEN, ENGINEIO_CLOSE, ENGINEIO_PING, ENGINEIO_PONG:
+ p.Type = t
+ p.Payload = b[1:]
+ case ENGINEIO_MESSAGE:
+ p.Type = t
+ mt, err := strconv.Atoi(string(b[1]))
+ if err != nil {
+ return p, err
+ }
+ p.MessageType = mt
+ p.Payload = b[2:]
+ }
+ return
+}
+
+func (g *GeneralsIO) RecievePackets() {
+ for {
+ _, packet, err := g.ReadMessage()
+ if err != nil {
+ l.Fatal(err)
+ }
+ p, err := DecodePacket(packet)
+ if err != nil {
+ l.Fatal(err)
+ }
+ switch p.Type {
+ case ENGINEIO_PONG:
+ l.Debug("Pong")
+ case ENGINEIO_OPEN:
+ l.Info("Open", zap.String("packet", string(p.Payload)))
+ case ENGINEIO_MESSAGE:
+ switch p.MessageType {
+ case SOCKETIO_CONNECT:
+ l.Debug("Connected")
+ case SOCKETIO_EVENT:
+ // generals.go
+ err = DecodeEvent(p.Payload, g.Updates)
+ if err != nil {
+ l.Error("Unhandled Event", zap.String("packet", string(packet)), zap.Error(err))
+ }
+ default:
+ l.Error("Unhandled Message", zap.String("packet", string(packet)))
+ }
+ default:
+ l.Error("Unhandled Packet", zap.String("packet", string(packet)))
+ }
+
+ }
+
+}