|
@@ -17,9 +17,12 @@ import (
|
|
|
)
|
|
|
|
|
|
type SnakeSimulator struct {
|
|
|
- field *Field
|
|
|
- snake *Snake
|
|
|
- stats *Stats
|
|
|
+ field *Field
|
|
|
+ snake *Snake
|
|
|
+ maxVerificationSteps int
|
|
|
+ stats *Stats
|
|
|
+
|
|
|
+ //GUI interface part
|
|
|
speed uint32
|
|
|
fieldUpdateQueue chan bool
|
|
|
snakeUpdateQueue chan bool
|
|
@@ -27,7 +30,8 @@ type SnakeSimulator struct {
|
|
|
speedQueue chan uint32
|
|
|
}
|
|
|
|
|
|
-func NewSnakeSimulator() (s *SnakeSimulator) {
|
|
|
+// Initializes new snake population with maximum number of verification steps
|
|
|
+func NewSnakeSimulator(maxVerificationSteps int) (s *SnakeSimulator) {
|
|
|
s = &SnakeSimulator{
|
|
|
field: &Field{
|
|
|
Food: &Point{},
|
|
@@ -41,19 +45,23 @@ func NewSnakeSimulator() (s *SnakeSimulator) {
|
|
|
&Point{X: 22, Y: 20},
|
|
|
},
|
|
|
},
|
|
|
- stats: &Stats{},
|
|
|
- fieldUpdateQueue: make(chan bool, 2),
|
|
|
- snakeUpdateQueue: make(chan bool, 2),
|
|
|
- statsUpdateQueue: make(chan bool, 2),
|
|
|
- speedQueue: make(chan uint32, 1),
|
|
|
- speed: 10,
|
|
|
+ stats: &Stats{},
|
|
|
+ maxVerificationSteps: maxVerificationSteps,
|
|
|
+ fieldUpdateQueue: make(chan bool, 2),
|
|
|
+ snakeUpdateQueue: make(chan bool, 2),
|
|
|
+ statsUpdateQueue: make(chan bool, 2),
|
|
|
+ speedQueue: make(chan uint32, 1),
|
|
|
+ speed: 10,
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+// Population test method
|
|
|
+// Verifies population and returns unsorted finteses for each individual
|
|
|
func (s *SnakeSimulator) Verify(population *genetic.Population) (fitnesses []*genetic.IndividalFitness) {
|
|
|
s.stats.Generation++
|
|
|
s.statsUpdateQueue <- true
|
|
|
+
|
|
|
s.field.GenerateNextFood()
|
|
|
s.fieldUpdateQueue <- true
|
|
|
|
|
@@ -64,13 +72,12 @@ func (s *SnakeSimulator) Verify(population *genetic.Population) (fitnesses []*ge
|
|
|
|
|
|
s.runSnake(inidividual, false)
|
|
|
fitnesses[index] = &genetic.IndividalFitness{
|
|
|
- Fitness: float64(len(s.snake.Points)-2) * float64(s.stats.Move),
|
|
|
- // Fitness: float64(s.stats.Move),
|
|
|
- Index: index,
|
|
|
+ Fitness: float64(s.stats.Move),
|
|
|
+ Index: index,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- //This is duplication of crossbreedPopulation functionality.
|
|
|
+ //This is duplication of crossbreedPopulation functionality to display best snake
|
|
|
sort.Slice(fitnesses, func(i, j int) bool {
|
|
|
return fitnesses[i].Fitness > fitnesses[j].Fitness //Descent order best will be on top, worst in the bottom
|
|
|
})
|
|
@@ -86,191 +93,108 @@ func (s *SnakeSimulator) Verify(population *genetic.Population) (fitnesses []*ge
|
|
|
func (s *SnakeSimulator) runSnake(inidividual *neuralnetwork.NeuralNetwork, randomStart bool) {
|
|
|
if randomStart {
|
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
- switch rand.Uint32() % 4 {
|
|
|
- case 1:
|
|
|
- s.snake = &Snake{
|
|
|
- Points: []*Point{
|
|
|
- &Point{X: 20, Y: 20},
|
|
|
- &Point{X: 21, Y: 20},
|
|
|
- &Point{X: 22, Y: 20},
|
|
|
- },
|
|
|
- }
|
|
|
- case 2:
|
|
|
- s.snake = &Snake{
|
|
|
- Points: []*Point{
|
|
|
- &Point{X: 22, Y: 20},
|
|
|
- &Point{X: 21, Y: 20},
|
|
|
- &Point{X: 20, Y: 20},
|
|
|
- },
|
|
|
- }
|
|
|
- case 3:
|
|
|
- s.snake = &Snake{
|
|
|
- Points: []*Point{
|
|
|
- &Point{X: 20, Y: 20},
|
|
|
- &Point{X: 20, Y: 21},
|
|
|
- &Point{X: 20, Y: 22},
|
|
|
- },
|
|
|
- }
|
|
|
- default:
|
|
|
- s.snake = &Snake{
|
|
|
- Points: []*Point{
|
|
|
- &Point{X: 20, Y: 22},
|
|
|
- &Point{X: 20, Y: 21},
|
|
|
- &Point{X: 20, Y: 20},
|
|
|
- },
|
|
|
- }
|
|
|
- }
|
|
|
+ s.snake = NewSnake(Direction(rand.Uint32()%4), *s.field)
|
|
|
} else {
|
|
|
- s.snake = &Snake{
|
|
|
- Points: []*Point{
|
|
|
- &Point{X: 19, Y: 20},
|
|
|
- &Point{X: 20, Y: 20},
|
|
|
- &Point{X: 21, Y: 20},
|
|
|
- },
|
|
|
- }
|
|
|
+ s.snake = NewSnake(Direction_Left, *s.field)
|
|
|
}
|
|
|
|
|
|
- i := 0
|
|
|
s.stats.Move = 0
|
|
|
- for i < 300 {
|
|
|
- if s.speed > 0 {
|
|
|
- s.statsUpdateQueue <- true
|
|
|
- }
|
|
|
-
|
|
|
- //Read speed from client
|
|
|
+ for i := 0; i < s.maxVerificationSteps; i++ {
|
|
|
+ //Read speed from client and sleep in case if user selected slow preview
|
|
|
select {
|
|
|
case newSpeed := <-s.speedQueue:
|
|
|
fmt.Printf("Apply new speed: %v\n", newSpeed)
|
|
|
- if newSpeed < 10 {
|
|
|
- if newSpeed > 0 {
|
|
|
- s.speed = newSpeed
|
|
|
- } else {
|
|
|
- s.speed = 0
|
|
|
- }
|
|
|
+ if newSpeed <= 10 && newSpeed >= 0 {
|
|
|
+ s.speed = newSpeed
|
|
|
+ } else if newSpeed < 0 {
|
|
|
+ s.speed = 0
|
|
|
}
|
|
|
default:
|
|
|
}
|
|
|
- i++
|
|
|
+
|
|
|
if s.speed > 0 {
|
|
|
+ time.Sleep(100 / time.Duration(s.speed) * time.Millisecond)
|
|
|
+ s.statsUpdateQueue <- true
|
|
|
s.snakeUpdateQueue <- true
|
|
|
}
|
|
|
- direction, _ := inidividual.Predict(mat.NewDense(inidividual.Sizes[0], 1, s.GetHeadState()))
|
|
|
- newHead := s.snake.NewHead(Direction(direction + 1))
|
|
|
-
|
|
|
- if selfCollisionIndex := s.snake.SelfCollision(newHead); selfCollisionIndex > 0 {
|
|
|
- if selfCollisionIndex == 1 {
|
|
|
- switch Direction(direction + 1) {
|
|
|
- case Direction_Up:
|
|
|
- newHead = s.snake.NewHead(Direction_Down)
|
|
|
- case Direction_Down:
|
|
|
- newHead = s.snake.NewHead(Direction_Up)
|
|
|
- case Direction_Left:
|
|
|
- newHead = s.snake.NewHead(Direction_Right)
|
|
|
- default:
|
|
|
- newHead = s.snake.NewHead(Direction_Left)
|
|
|
- }
|
|
|
- // continue
|
|
|
- } else {
|
|
|
- fmt.Printf("Game over self collision\n")
|
|
|
- break
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- if newHead.X == s.field.Food.X && newHead.Y == s.field.Food.Y {
|
|
|
+ predictIndex, _ := inidividual.Predict(mat.NewDense(inidividual.Sizes[0], 1, s.getHeadState()))
|
|
|
+ direction := Direction(predictIndex + 1)
|
|
|
+ newHead := s.snake.NewHead(direction)
|
|
|
+
|
|
|
+ if s.snake.selfCollision(newHead, direction) {
|
|
|
+ fmt.Printf("Game over self collision\n")
|
|
|
+ break
|
|
|
+ } else if wallCollision(newHead, *s.field) {
|
|
|
+ break
|
|
|
+ } else if foodCollision(newHead, s.field.Food) {
|
|
|
i = 0
|
|
|
s.snake.Feed(newHead)
|
|
|
s.field.GenerateNextFood()
|
|
|
s.fieldUpdateQueue <- true
|
|
|
- } else if newHead.X >= s.field.Width || newHead.Y >= s.field.Height || newHead.X < 0 || newHead.Y < 0 {
|
|
|
- // fmt.Printf("Game over\n")
|
|
|
- // time.Sleep(1000 * time.Millisecond)
|
|
|
- break
|
|
|
} else {
|
|
|
s.snake.Move(newHead)
|
|
|
}
|
|
|
- if s.speed > 0 {
|
|
|
- time.Sleep(100 / time.Duration(s.speed) * time.Millisecond)
|
|
|
- }
|
|
|
s.stats.Move++
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (s *SnakeSimulator) GetHeadState() []float64 {
|
|
|
+// Produces input activations for neural network
|
|
|
+func (s *SnakeSimulator) getHeadState() []float64 {
|
|
|
+ // Snake state
|
|
|
headX := float64(s.snake.Points[0].X)
|
|
|
headY := float64(s.snake.Points[0].Y)
|
|
|
tailX := float64(s.snake.Points[len(s.snake.Points)-1].X)
|
|
|
tailY := float64(s.snake.Points[len(s.snake.Points)-1].Y)
|
|
|
- // prevX := float64(s.snake.Points[1].X)
|
|
|
- // prevY := float64(s.snake.Points[1].Y)
|
|
|
+
|
|
|
+ // Field state
|
|
|
foodX := float64(s.field.Food.X)
|
|
|
foodY := float64(s.field.Food.Y)
|
|
|
width := float64(s.field.Width)
|
|
|
height := float64(s.field.Height)
|
|
|
- diag := float64(width) * math.Sqrt2
|
|
|
+ diag := float64(width) * math.Sqrt2 //We assume that field is always square
|
|
|
|
|
|
+ // Output activations
|
|
|
+ // Distance to walls in 4 directions
|
|
|
lWall := headX
|
|
|
rWall := (width - headX)
|
|
|
tWall := headY
|
|
|
bWall := (height - headY)
|
|
|
|
|
|
+ // Distance to walls in 4 diagonal directions, by default is completely inactive
|
|
|
+ tlWall := float64(0)
|
|
|
+ trWall := float64(0)
|
|
|
+ blWall := float64(0)
|
|
|
+ brWall := float64(0)
|
|
|
+
|
|
|
+ // Distance to food in 4 directions
|
|
|
+ // By default is size of field that means that there is no activation at all
|
|
|
lFood := float64(width)
|
|
|
rFood := float64(width)
|
|
|
tFood := float64(height)
|
|
|
bFood := float64(height)
|
|
|
- if headX == foodX {
|
|
|
- tFood = headY - foodY
|
|
|
- if tFood < 0 {
|
|
|
- tFood = height
|
|
|
- } else {
|
|
|
- tFood = 0
|
|
|
- }
|
|
|
- bFood = foodY - headY
|
|
|
- if bFood < 0 {
|
|
|
- bFood = height
|
|
|
- } else {
|
|
|
- bFood = 0
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if headY == foodY {
|
|
|
- rFood = foodX - headX
|
|
|
- if rFood < 0 {
|
|
|
- rFood = width
|
|
|
- } else {
|
|
|
- rFood = 0
|
|
|
- }
|
|
|
- lFood = headX - foodX
|
|
|
- if lFood < 0 {
|
|
|
- lFood = width
|
|
|
- } else {
|
|
|
- lFood = 0
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
+ // Distance to food in 4 diagonal directions
|
|
|
+ // By default is size of field diagonal that means that there is no activation
|
|
|
+ // at all
|
|
|
tlFood := float64(diag)
|
|
|
trFood := float64(diag)
|
|
|
blFood := float64(diag)
|
|
|
brFood := float64(diag)
|
|
|
- if math.Abs(foodY-headY) == math.Abs(foodX-headX) {
|
|
|
- if foodX > headX {
|
|
|
- if foodY > headY {
|
|
|
- trFood = 0 //math.Abs(foodX-headX) * math.Sqrt2
|
|
|
- } else {
|
|
|
- brFood = 0 //math.Abs(foodX-headX) * math.Sqrt2
|
|
|
- }
|
|
|
- } else {
|
|
|
- if foodY > headY {
|
|
|
- tlFood = 0 //math.Abs(foodX-headX) * math.Sqrt2
|
|
|
- } else {
|
|
|
- blFood = 0 //math.Abs(foodX-headX) * math.Sqrt2
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- tlWall := float64(0)
|
|
|
- trWall := float64(0)
|
|
|
- blWall := float64(0)
|
|
|
- brWall := float64(0)
|
|
|
+ // Distance to tail in 4 directions
|
|
|
+ tTail := float64(0)
|
|
|
+ bTail := float64(0)
|
|
|
+ lTail := float64(0)
|
|
|
+ rTail := float64(0)
|
|
|
+
|
|
|
+ // Distance to tail in 4 diagonal directions
|
|
|
+ tlTail := float64(0)
|
|
|
+ trTail := float64(0)
|
|
|
+ blTail := float64(0)
|
|
|
+ brTail := float64(0)
|
|
|
+
|
|
|
+ // Diagonal distance to each wall
|
|
|
if lWall > tWall {
|
|
|
tlWall = float64(tWall) * math.Sqrt2
|
|
|
} else {
|
|
@@ -295,80 +219,78 @@ func (s *SnakeSimulator) GetHeadState() []float64 {
|
|
|
brWall = float64(rWall) * math.Sqrt2
|
|
|
}
|
|
|
|
|
|
- if lWall > tWall {
|
|
|
- tlWall = float64(tWall) * math.Sqrt2
|
|
|
- } else {
|
|
|
- tlWall = float64(lWall) * math.Sqrt2
|
|
|
- }
|
|
|
-
|
|
|
- if rWall > tWall {
|
|
|
- trWall = float64(tWall) * math.Sqrt2
|
|
|
- } else {
|
|
|
- trWall = float64(rWall) * math.Sqrt2
|
|
|
+ // Check if food is on same vertical line with head and
|
|
|
+ // choose vertical direction for activation
|
|
|
+ if headX == foodX {
|
|
|
+ if headY-foodY < 0 {
|
|
|
+ bFood = 0
|
|
|
+ } else {
|
|
|
+ tFood = 0
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if lWall > bWall {
|
|
|
- blWall = float64(bWall) * math.Sqrt2
|
|
|
- } else {
|
|
|
- blWall = float64(lWall) * math.Sqrt2
|
|
|
+ // Check if food is on same horizontal line with head and
|
|
|
+ // choose horizontal direction for activation
|
|
|
+ if headY == foodY {
|
|
|
+ if foodX-headX < 0 {
|
|
|
+ lFood = 0
|
|
|
+ } else {
|
|
|
+ rFood = 0
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if rWall > bWall {
|
|
|
- blWall = float64(bWall) * math.Sqrt2
|
|
|
- } else {
|
|
|
- brWall = float64(rWall) * math.Sqrt2
|
|
|
+ //Check if food is on diagonal any of 4 ways
|
|
|
+ if math.Abs(foodY-headY) == math.Abs(foodX-headX) {
|
|
|
+ //Choose diagonal direction to food
|
|
|
+ if foodX > headX {
|
|
|
+ if foodY > headY {
|
|
|
+ trFood = 0
|
|
|
+ } else {
|
|
|
+ brFood = 0
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if foodY > headY {
|
|
|
+ tlFood = 0
|
|
|
+ } else {
|
|
|
+ blFood = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- tTail := float64(0)
|
|
|
- bTail := float64(0)
|
|
|
+ // Check if tail is on same vertical line with head and
|
|
|
+ // choose vertical direction for activation
|
|
|
if headX == tailX {
|
|
|
- tTail = (headY - tailY)
|
|
|
- if tTail < 0 {
|
|
|
- tTail = height
|
|
|
- } else {
|
|
|
- tTail = 0
|
|
|
- }
|
|
|
- bTail = (tailY - headY)
|
|
|
- if bTail < 0 {
|
|
|
+ if headY-tailY < 0 {
|
|
|
bTail = height
|
|
|
} else {
|
|
|
- bTail = 0
|
|
|
+ tTail = height
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- lTail := float64(0)
|
|
|
- rTail := float64(0)
|
|
|
+ // Check if tail is on same horizontal line with head and
|
|
|
+ // choose horizontal direction for activation
|
|
|
if headY == tailY {
|
|
|
- lTail = (headX - tailX)
|
|
|
- if lTail < 0 {
|
|
|
- tTail = width
|
|
|
- } else {
|
|
|
- tTail = 0
|
|
|
- }
|
|
|
- rTail = (tailX - headX)
|
|
|
- if lTail < 0 {
|
|
|
- tTail = width
|
|
|
+ if headX-tailX < 0 {
|
|
|
+ rTail = width
|
|
|
} else {
|
|
|
- tTail = 0
|
|
|
+ lTail = width
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- tlTail := float64(0)
|
|
|
- trTail := float64(0)
|
|
|
- blTail := float64(0)
|
|
|
- brTail := float64(0)
|
|
|
+ //Check if tail is on diagonal any of 4 ways
|
|
|
if math.Abs(headY-tailY) == math.Abs(headX-tailX) {
|
|
|
+ //Choose diagonal direction to tail
|
|
|
if tailY > headY {
|
|
|
if tailX > headX {
|
|
|
- trTail = diag //math.Abs(tailX-headX) * math.Sqrt2
|
|
|
+ trTail = diag
|
|
|
} else {
|
|
|
- tlTail = diag //math.Abs(tailX-headX) * math.Sqrt2
|
|
|
+ tlTail = diag
|
|
|
}
|
|
|
} else {
|
|
|
if tailX > headX {
|
|
|
- brTail = diag //math.Abs(tailX-headX) * math.Sqrt2
|
|
|
+ brTail = diag
|
|
|
} else {
|
|
|
- blTail = diag //math.Abs(tailX-headX) * math.Sqrt2
|
|
|
+ blTail = diag
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -378,14 +300,14 @@ func (s *SnakeSimulator) GetHeadState() []float64 {
|
|
|
rWall / width,
|
|
|
tWall / height,
|
|
|
bWall / height,
|
|
|
- (1.0 - lFood/width),
|
|
|
- (1.0 - rFood/width),
|
|
|
- (1.0 - tFood/height),
|
|
|
- (1.0 - bFood/height),
|
|
|
tlWall / diag,
|
|
|
trWall / diag,
|
|
|
blWall / diag,
|
|
|
brWall / diag,
|
|
|
+ (1.0 - lFood/width),
|
|
|
+ (1.0 - rFood/width),
|
|
|
+ (1.0 - tFood/height),
|
|
|
+ (1.0 - bFood/height),
|
|
|
(1.0 - tlFood/diag),
|
|
|
(1.0 - trFood/diag),
|
|
|
(1.0 - blFood/diag),
|
|
@@ -401,6 +323,9 @@ func (s *SnakeSimulator) GetHeadState() []float64 {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Server part
|
|
|
+
|
|
|
+// Runs gRPC server for GUI
|
|
|
func (s *SnakeSimulator) StartServer() {
|
|
|
go func() {
|
|
|
grpcServer := grpc.NewServer()
|
|
@@ -417,33 +342,7 @@ func (s *SnakeSimulator) StartServer() {
|
|
|
}()
|
|
|
}
|
|
|
|
|
|
-// func (s *SnakeSimulator) Run() {
|
|
|
-// s.field.GenerateNextFood()
|
|
|
-// for true {
|
|
|
-// direction := rand.Int31()%4 + 1
|
|
|
-// newHead := s.snake.NewHead(Direction(direction))
|
|
|
-// if newHead.X == s.field.Food.X && newHead.Y == s.field.Food.Y {
|
|
|
-// s.snake.Feed(newHead)
|
|
|
-// s.field.GenerateNextFood()
|
|
|
-
|
|
|
-// } else if newHead.X > s.field.Width || newHead.Y > s.field.Height {
|
|
|
-// fmt.Printf("Game over\n")
|
|
|
-// break
|
|
|
-// } else if selfCollisionIndex := s.snake.SelfCollision(newHead); selfCollisionIndex > 0 {
|
|
|
-// if selfCollisionIndex == 1 {
|
|
|
-// fmt.Printf("Step backward, skip\n")
|
|
|
-// continue
|
|
|
-// }
|
|
|
-// fmt.Printf("Game over self collision\n")
|
|
|
-// break
|
|
|
-// } else {
|
|
|
-// s.snake.Move(newHead)
|
|
|
-// }
|
|
|
-// s.snakeUpdateQueue <- true
|
|
|
-// time.Sleep(50 * time.Millisecond)
|
|
|
-// }
|
|
|
-// }
|
|
|
-
|
|
|
+// Steaming of Field updates
|
|
|
func (s *SnakeSimulator) Field(_ *None, srv SnakeSimulator_FieldServer) error {
|
|
|
ctx := srv.Context()
|
|
|
for {
|
|
@@ -457,6 +356,7 @@ func (s *SnakeSimulator) Field(_ *None, srv SnakeSimulator_FieldServer) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Steaming of Snake position and length updates
|
|
|
func (s *SnakeSimulator) Snake(_ *None, srv SnakeSimulator_SnakeServer) error {
|
|
|
ctx := srv.Context()
|
|
|
for {
|
|
@@ -470,6 +370,7 @@ func (s *SnakeSimulator) Snake(_ *None, srv SnakeSimulator_SnakeServer) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Steaming of snake simulator statistic
|
|
|
func (s *SnakeSimulator) Stats(_ *None, srv SnakeSimulator_StatsServer) error {
|
|
|
ctx := srv.Context()
|
|
|
for {
|
|
@@ -483,6 +384,7 @@ func (s *SnakeSimulator) Stats(_ *None, srv SnakeSimulator_StatsServer) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Setup new speed requested from gRPC GUI client
|
|
|
func (s *SnakeSimulator) SetSpeed(ctx context.Context, speed *Speed) (*None, error) {
|
|
|
s.speedQueue <- speed.Speed
|
|
|
return &None{}, nil
|