Commit dd9b3d7

bryfry <bryon@fryer.io>
2017-02-19 23:43:29
basic map updating working
1 parent 9a7db8d
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)))
+		}
+
+	}
+
+}