Skip to content

Commit

Permalink
Add MVP
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan1993spb committed Dec 12, 2021
1 parent 321b0a0 commit 104880c
Show file tree
Hide file tree
Showing 51 changed files with 4,590 additions and 0 deletions.
70 changes: 70 additions & 0 deletions cmd/snake-bot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"math/rand"
"os"
"os/signal"
"time"

"github.com/sirupsen/logrus"

"github.com/ivan1993spb/snake-bot/internal/config"
"github.com/ivan1993spb/snake-bot/internal/connect"
"github.com/ivan1993spb/snake-bot/internal/core"
"github.com/ivan1993spb/snake-bot/internal/secure"
"github.com/ivan1993spb/snake-bot/internal/server"
"github.com/ivan1993spb/snake-bot/internal/utils"
)

const ApplicationName = "Snake-Bot"

var (
Version = "dev"
Build = "dev"
)

func init() {
rand.Seed(time.Now().UnixNano())
}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

cfg, err := config.StdConfig()

{
logger := utils.NewLogger(cfg.Log)
ctx = utils.LogContext(ctx, logger)
}

if err != nil {
utils.Log(ctx).WithError(err).Fatal("config fail")
}

utils.Log(ctx).WithFields(logrus.Fields{
"version": Version,
"build": Build,
}).Info("Welcome to Snake-Bot!")

sec := secure.NewSecure()
if err := sec.GenerateToken(os.Stdout); err != nil {
utils.Log(ctx).WithError(err).Fatal("security fail")
}
utils.Log(ctx).Warn("auth token successfully generated")

headerAppInfo := utils.FormatAppInfoHeader(ApplicationName, Version, Build)

connector := connect.NewConnector(cfg.Target, headerAppInfo)

c := core.NewCore(ctx, connector, cfg.Bots.Limit)

serv := server.NewServer(ctx, cfg.Server, headerAppInfo, c, sec)

if err := serv.ListenAndServe(ctx); err != nil {
utils.Log(ctx).WithError(err).Fatal("server error")
}

utils.Log(ctx).Info("buh bye!")
}
6 changes: 6 additions & 0 deletions embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package snakebot

import _ "embed"

//go:embed api/openapi.yaml
var OpenAPISpec string
18 changes: 18 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/ivan1993spb/snake-bot

go 1.16

require (
github.com/go-chi/chi/v5 v5.0.5
github.com/go-chi/cors v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/kr/text v0.2.0 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.0
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
lukechampine.com/noescape v0.0.0-20191006153127-214c369a3d1b
)
167 changes: 167 additions & 0 deletions go.sum

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions internal/bot/bot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package bot

import (
"context"
"math/rand"
"sync/atomic"
"time"

"github.com/sirupsen/logrus"

"github.com/ivan1993spb/snake-bot/internal/bot/engine"
"github.com/ivan1993spb/snake-bot/internal/types"
"github.com/ivan1993spb/snake-bot/internal/utils"
)

const botTickTime = time.Millisecond * 200

const lookupDistance uint8 = 50

const directionExpireTime = time.Millisecond * 10

const (
stateWait = iota
stateExplore
)

type World interface {
LookAround(sight engine.Sight) *engine.HashmapSight
GetArea() engine.Area
GetObject(id uint32) (*types.Object, bool)
}

type Bot struct {
state uint32

myId uint32
world World

discoverer engine.Discoverer

lastPosition types.Dot
lastDirection types.Direction
}

func NewBot(world World, discoverer engine.Discoverer) *Bot {
return &Bot{
world: world,
discoverer: discoverer,
}
}

func NewDijkstrasBot(world World) *Bot {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
discoverer := engine.NewDijkstrasDiscoverer(r)
cacher := engine.NewCacherDiscoverer(discoverer)
return NewBot(world, cacher)
}

func (b *Bot) Run(ctx context.Context, stop <-chan struct{}) <-chan types.Direction {
chout := make(chan types.Direction)

go func() {
defer close(chout)
ticker := time.NewTicker(botTickTime)
defer ticker.Stop()

for {
select {
case <-ticker.C:
direction, ok := b.operate(ctx)
if ok {
ctx, cancel := context.WithTimeout(ctx, directionExpireTime)
select {
case chout <- direction:
case <-ctx.Done():
}
cancel()
}
case <-stop:
return
case <-ctx.Done():
return
}
}
}()

return chout
}

func (b *Bot) getState() uint32 {
return atomic.LoadUint32(&b.state)
}

func (b *Bot) getMe() uint32 {
return atomic.LoadUint32(&b.myId)
}

func (b *Bot) operate(ctx context.Context) (types.Direction, bool) {
if b.getState() != stateExplore {
return types.DirectionZero, false
}

me, ok := b.world.GetObject(b.getMe())
if !ok {
return types.DirectionZero, false
}

area := b.world.GetArea()
objectDots := me.GetDots()
if len(objectDots) == 0 {
return types.DirectionZero, false
}
head := objectDots[0]
sight := engine.NewSight(area, head, lookupDistance)

objects := b.world.LookAround(sight)
scores := b.score(objects)

path := b.discoverer.Discover(head, area, sight, scores)
if len(path) == 0 {
return types.DirectionZero, false
}
direction := area.FindDirection(head, path[0])

if area.FindDirection(objectDots[1], objectDots[0]) == direction {
// the same direction
return types.DirectionZero, false
}
if b.lastDirection != direction || b.lastPosition != head {
utils.Log(ctx).WithFields(logrus.Fields{
"head": head,
"direction": direction,
}).Debugln("change direction")
b.lastDirection = direction
b.lastPosition = head

return direction, true
}
return types.DirectionZero, false
}

func (b *Bot) Countdown(sec int) {
// TODO: Shut down the snake if the stateWait
atomic.StoreUint32(&b.state, stateWait)
}

func (b *Bot) Me(id uint32) {
atomic.StoreUint32(&b.myId, id)
atomic.StoreUint32(&b.state, stateExplore)
}
67 changes: 67 additions & 0 deletions internal/bot/bot_score.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package bot

import (
"github.com/ivan1993spb/snake-bot/internal/bot/engine"
"github.com/ivan1993spb/snake-bot/internal/types"
)

type scoreType uint8

const (
scoreTypeCollapse scoreType = iota
scoreTypeFoodApple
scoreTypeFoodCorpse
scoreTypeFoodWatermelon
scoreTypeFoodMouse
scoreTypeHunt
)

var defaultBehavior = map[scoreType]int{
scoreTypeCollapse: -1000,
scoreTypeFoodApple: 1,
scoreTypeFoodCorpse: 2,
scoreTypeFoodWatermelon: 5,
scoreTypeFoodMouse: 15,
scoreTypeHunt: 30,
}

func (b *Bot) score(objects *engine.HashmapSight) *engine.HashmapSight {
// TODO: Estimate the real prey length.
const preyLen = 3

scores := objects.Reflect()

objects.ForEach(func(dot types.Dot, v interface{}) {
object, ok := v.(*types.Object)
if !ok {
return
}
if object.Id == b.myId {
scores.Assign(dot, defaultBehavior[scoreTypeCollapse])
} else if object.Type == types.ObjectTypeSnake {
if len(object.Dots) == preyLen {
scores.Assign(dot, defaultBehavior[scoreTypeHunt])
} else {
scores.Assign(dot, defaultBehavior[scoreTypeCollapse])
}
} else if object.Type == types.ObjectTypeWall {
scores.Assign(dot, defaultBehavior[scoreTypeCollapse])
} else {
switch object.Type {
case types.ObjectTypeApple:
scores.Assign(dot, defaultBehavior[scoreTypeFoodApple])
case types.ObjectTypeCorpse:
scores.Assign(dot, defaultBehavior[scoreTypeFoodCorpse])
case types.ObjectTypeWatermelon:
scores.Assign(dot, defaultBehavior[scoreTypeFoodWatermelon])
case types.ObjectTypeMouse:
scores.Assign(dot, defaultBehavior[scoreTypeFoodMouse])
default:
// Avoid unknown objects.
scores.Assign(dot, defaultBehavior[scoreTypeCollapse])
}
}
})

return scores
}
Loading

0 comments on commit 104880c

Please sign in to comment.