Jelajahi Sumber

Initial commit of snake simulator

Alexey Edelev 5 tahun lalu
melakukan
33e229e64b

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+src
+bin
+pkg
+CMakeLists.txt.user
+*.pb.go

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "snakesimulatorui/qtprotobuf"]
+	path = snakesimulatorui/qtprotobuf
+	url = git@git.semlanik.org:semlanik/qtprotobuf.git

+ 8 - 0
LICENSE

@@ -0,0 +1,8 @@
+MIT License
+Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>, Tatyana Borisova <tanusshhka@mail.ru>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 13 - 0
build.sh

@@ -0,0 +1,13 @@
+export GOPATH=$PWD
+export PATH=$PATH:$PWD/bin
+export GOBIN=$PWD/bin
+
+go get github.com/golang/protobuf/protoc-gen-go
+go install ./src/github.com/golang/protobuf/protoc-gen-go
+
+export SNAKE_RPC_PATH=$PWD/snakesimulator
+mkdir -p $SNAKE_RPC_PATH
+rm -f $SNAKE_RPC_PATH/*.pb.go
+protoc -I$SNAKE_RPC_PATH --go_out=plugins=grpc:$SNAKE_RPC_PATH $SNAKE_RPC_PATH/snakesimulator.proto
+go get -v
+go build -o $GOBIN/snakesimulator

+ 43 - 0
main.go

@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package main
+
+import (
+	genetic "git.semlanik.org/semlanik/NeuralNetwork/genetic"
+	mutagens "git.semlanik.org/semlanik/NeuralNetwork/genetic/mutagens"
+	snakesimulator "./snakesimulator"
+)
+
+func main() {
+	s := snakesimulator.NewSnakeSimulator(400)
+	s.StartServer()
+
+	p := genetic.NewPopulation(s, mutagens.NewDummyMutagen(1.0, 1), genetic.PopulationConfig{PopulationSize: 2000, SelectionSize: 0.01, CrossbreedPart: 0.5}, []int{24, 18, 18, 4})
+
+	p.NaturalSelection(5000)
+
+	s.PlayBestNetwork(p.GetBestNetwork())
+}

+ 37 - 0
snakesimulator/field.go

@@ -0,0 +1,37 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package snakesimulator
+
+import (
+	"math/rand"
+	"time"
+)
+
+func (f *Field) GenerateNextFood() {
+	rand.Seed(time.Now().UnixNano())
+	f.Food.X = rand.Uint32() % f.Width
+	f.Food.Y = rand.Uint32() % f.Height
+}

+ 131 - 0
snakesimulator/snake.go

@@ -0,0 +1,131 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package snakesimulator
+
+func (p Point) Copy() (pCopy *Point) {
+	pCopy = &Point{
+		X: p.X,
+		Y: p.Y,
+	}
+	return
+}
+
+func NewSnake(direction Direction, field Field) (s *Snake) {
+	fieldCenterX := field.Width / 2
+	fieldCenterY := field.Height / 2
+	switch direction {
+	case Direction_Left:
+		s = &Snake{
+			Points: []*Point{
+				&Point{X: fieldCenterX - 1, Y: fieldCenterY},
+				&Point{X: fieldCenterX, Y: fieldCenterY},
+				&Point{X: fieldCenterX + 1, Y: fieldCenterY},
+			},
+		}
+	case Direction_Right:
+		s = &Snake{
+			Points: []*Point{
+				&Point{X: fieldCenterX + 1, Y: fieldCenterY},
+				&Point{X: fieldCenterX, Y: fieldCenterY},
+				&Point{X: fieldCenterX - 1, Y: fieldCenterY},
+			},
+		}
+	case Direction_Down:
+		s = &Snake{
+			Points: []*Point{
+				&Point{X: fieldCenterX, Y: fieldCenterY - 1},
+				&Point{X: fieldCenterX, Y: fieldCenterY},
+				&Point{X: fieldCenterX, Y: fieldCenterY + 1},
+			},
+		}
+	default:
+		s = &Snake{
+			Points: []*Point{
+				&Point{X: fieldCenterX, Y: fieldCenterY + 1},
+				&Point{X: fieldCenterX, Y: fieldCenterY},
+				&Point{X: fieldCenterX, Y: fieldCenterY - 1},
+			},
+		}
+	}
+	return
+}
+
+func (s *Snake) NewHead(direction Direction) (newHead *Point) {
+	newHead = s.Points[0].Copy()
+	switch direction {
+	case Direction_Up:
+		newHead.Y -= 1
+	case Direction_Down:
+		newHead.Y += 1
+	case Direction_Right:
+		newHead.X += 1
+	case Direction_Left:
+		newHead.X -= 1
+	}
+	return
+}
+
+func (s *Snake) Move(newHead *Point) {
+	s.Points = s.Points[:len(s.Points)-1]
+	s.Points = append([]*Point{newHead}, s.Points...)
+}
+
+func (s *Snake) Feed(food *Point) {
+	s.Points = append([]*Point{food}, s.Points...)
+}
+
+func (s *Snake) selfCollision(head *Point, direction Direction) bool {
+	selfCollisionIndex := -1
+	for index, point := range s.Points[:len(s.Points)-1] {
+		if point.X == head.X && point.Y == head.Y {
+			selfCollisionIndex = index
+			break
+		}
+	}
+
+	if selfCollisionIndex == 1 {
+		switch direction {
+		case Direction_Up:
+			head.Y += 2
+		case Direction_Down:
+			head.Y -= 2
+		case Direction_Left:
+			head.X += 2
+		default:
+			head.X -= 2
+		}
+		return false
+	}
+	return selfCollisionIndex >= 0
+}
+
+func wallCollision(head *Point, field Field) bool {
+	return head.X >= field.Width || head.Y >= field.Height || head.X < 0 || head.Y < 0
+}
+
+func foodCollision(head *Point, food *Point) bool {
+	return head.X == food.X && head.Y == food.Y
+}

+ 520 - 0
snakesimulator/snakesimulator.go

@@ -0,0 +1,520 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package snakesimulator
+
+import (
+	context "context"
+	fmt "fmt"
+	math "math"
+	"math/rand"
+	"net"
+	"sort"
+	"sync"
+	"time"
+
+	"gonum.org/v1/gonum/mat"
+
+	genetic "git.semlanik.org/semlanik/NeuralNetwork/genetic"
+	neuralnetwork "git.semlanik.org/semlanik/NeuralNetwork/neuralnetwork"
+	remotecontrol "git.semlanik.org/semlanik/NeuralNetwork/remotecontrol"
+	grpc "google.golang.org/grpc"
+)
+
+type SnakeSimulator struct {
+	field                *Field
+	snake                *Snake
+	maxVerificationSteps int
+	stats                *Stats
+	remoteControl        *remotecontrol.RemoteControl
+
+	//GUI interface part
+	speed                uint32
+	fieldUpdateQueue     chan bool
+	snakeUpdateQueue     chan bool
+	statsUpdateQueue     chan bool
+	isPlayingUpdateQueue chan bool
+	speedQueue           chan uint32
+	isPlaying            bool
+	repeatInLoop         bool
+
+	snakeReadMutex sync.Mutex
+	fieldReadMutex sync.Mutex
+}
+
+// Initializes new snake population with maximum number of verification steps
+func NewSnakeSimulator(maxVerificationSteps int) (s *SnakeSimulator) {
+	s = &SnakeSimulator{
+		field: &Field{
+			Food:   &Point{},
+			Width:  40,
+			Height: 40,
+		},
+		snake: &Snake{
+			Points: []*Point{
+				&Point{X: 20, Y: 20},
+				&Point{X: 21, Y: 20},
+				&Point{X: 22, Y: 20},
+			},
+		},
+		stats:                &Stats{},
+		maxVerificationSteps: maxVerificationSteps,
+		fieldUpdateQueue:     make(chan bool, 2),
+		snakeUpdateQueue:     make(chan bool, 2),
+		statsUpdateQueue:     make(chan bool, 2),
+		isPlayingUpdateQueue: make(chan bool, 1),
+		speedQueue:           make(chan uint32, 1),
+		speed:                10,
+		remoteControl:        remotecontrol.NewRemoteControl(),
+	}
+	return
+}
+
+// Population test method
+// Verifies population and returns unsorted finteses for each individual
+func (s *SnakeSimulator) Verify(population *genetic.Population) (fitnesses []*genetic.IndividalFitness) {
+	s.remoteControl.Init(population.Networks[0])
+	s.stats.Generation++
+	s.statsUpdateQueue <- true
+
+	s.field.GenerateNextFood()
+	if s.speed > 0 {
+		s.fieldUpdateQueue <- true
+	}
+
+	fitnesses = make([]*genetic.IndividalFitness, len(population.Networks))
+	for index, inidividual := range population.Networks {
+		s.stats.Individual = uint32(index)
+		s.statsUpdateQueue <- true
+
+		s.runSnake(inidividual, false)
+		fitnesses[index] = &genetic.IndividalFitness{
+			// Fitness: float64(s.stats.Move), //Uncomment this to decrese food impact to individual selection
+			Fitness: float64(s.stats.Move) * float64(len(s.snake.Points)-2),
+			Index:   index,
+		}
+	}
+
+	//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
+	})
+
+	//Best snake showtime!
+	s.fieldReadMutex.Lock()
+	s.field.GenerateNextFood()
+	s.fieldReadMutex.Unlock()
+	s.fieldUpdateQueue <- true
+	prevSpeed := s.speed
+	s.speed = 5
+	if s.isPlaying == true {
+		// Play best of the best
+		s.isPlaying = false
+		s.isPlayingUpdateQueue <- s.isPlaying
+		population.GetBestNetwork().SetStateWatcher(s.remoteControl)
+		s.runSnake(population.GetBestNetwork(), false)
+		population.GetBestNetwork().SetStateWatcher(nil)
+	} else {
+		// Pley best from generation
+		population.Networks[fitnesses[0].Index].SetStateWatcher(s.remoteControl)
+		s.runSnake(population.Networks[fitnesses[0].Index], false)
+		population.Networks[fitnesses[0].Index].SetStateWatcher(nil)
+	}
+	s.speed = prevSpeed
+	return
+}
+
+func (s *SnakeSimulator) PlayBestNetwork(network *neuralnetwork.NeuralNetwork) {
+
+	for s.repeatInLoop {
+		s.remoteControl.Init(network)
+		s.stats.Generation++
+		s.statsUpdateQueue <- true
+
+		s.field.GenerateNextFood()
+		if s.speed > 0 {
+			s.fieldUpdateQueue <- true
+		}
+
+		//Best snake showtime!
+		s.fieldReadMutex.Lock()
+		s.field.GenerateNextFood()
+		s.fieldReadMutex.Unlock()
+		s.fieldUpdateQueue <- true
+		s.isPlaying = false
+		s.isPlayingUpdateQueue <- s.isPlaying
+		prevSpeed := s.speed
+		s.speed = 5
+		network.SetStateWatcher(s.remoteControl)
+		s.runSnake(network, false)
+		network.SetStateWatcher(nil)
+		s.speed = prevSpeed
+	}
+}
+
+func (s *SnakeSimulator) runSnake(inidividual *neuralnetwork.NeuralNetwork, randomStart bool) {
+	s.snakeReadMutex.Lock()
+	if randomStart {
+		rand.Seed(time.Now().UnixNano())
+		s.snake = NewSnake(Direction(rand.Uint32()%4), *s.field)
+	} else {
+		s.snake = NewSnake(Direction_Left, *s.field)
+	}
+	s.snakeReadMutex.Unlock()
+
+	s.stats.Move = 0
+	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 && newSpeed >= 0 {
+				s.speed = newSpeed
+			} else if newSpeed < 0 {
+				s.speed = 0
+			}
+		default:
+		}
+
+		if s.speed > 0 {
+			time.Sleep(100 / time.Duration(s.speed) * time.Millisecond)
+			s.statsUpdateQueue <- true
+			s.snakeUpdateQueue <- true
+		}
+
+		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.snakeReadMutex.Lock()
+			s.snake.Feed(newHead)
+			s.snakeReadMutex.Unlock()
+			s.fieldReadMutex.Lock()
+			s.field.GenerateNextFood()
+			s.fieldReadMutex.Unlock()
+			if s.speed > 0 {
+				s.fieldUpdateQueue <- true
+			}
+		} else {
+			s.snakeReadMutex.Lock()
+			s.snake.Move(newHead)
+			s.snakeReadMutex.Unlock()
+		}
+		s.stats.Move++
+	}
+}
+
+// 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)
+
+	// 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 //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)
+
+	// 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)
+
+	// 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 {
+		tlWall = float64(lWall) * math.Sqrt2
+	}
+
+	if rWall > tWall {
+		trWall = float64(tWall) * math.Sqrt2
+	} else {
+		trWall = float64(rWall) * math.Sqrt2
+	}
+
+	if lWall > bWall {
+		blWall = float64(bWall) * math.Sqrt2
+	} else {
+		blWall = float64(lWall) * math.Sqrt2
+	}
+
+	if rWall > bWall {
+		blWall = float64(bWall) * math.Sqrt2
+	} else {
+		brWall = 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 {
+			tFood = 0
+		} else {
+			bFood = 0
+		}
+	}
+
+	// Check if food is on same horizontal line with head and
+	// choose horizontal direction for activation
+	if headY == foodY {
+		if headX-foodX > 0 {
+			lFood = 0
+		} else {
+			rFood = 0
+		}
+	}
+
+	//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
+			}
+		}
+	}
+
+	// Check if tail is on same vertical line with head and
+	// choose vertical direction for activation
+	if headX == tailX {
+		if headY-tailY > 0 {
+			tTail = headY - tailY
+		} else {
+			bTail = headY - tailY
+		}
+	}
+
+	// Check if tail is on same horizontal line with head and
+	// choose horizontal direction for activation
+	if headY == tailY {
+		if headX-tailX > 0 {
+			rTail = headX - tailX
+		} else {
+			lTail = headX - tailX
+		}
+	}
+
+	//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
+			} else {
+				tlTail = diag
+			}
+		} else {
+			if tailX > headX {
+				brTail = diag
+			} else {
+				blTail = diag
+			}
+		}
+	}
+
+	return []float64{
+		lWall / width,
+		rWall / width,
+		tWall / height,
+		bWall / 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),
+		(1.0 - brFood/diag),
+		tTail / height,
+		bTail / height,
+		lTail / width,
+		rTail / width,
+		tlTail / diag,
+		trTail / diag,
+		blTail / diag,
+		brTail / diag,
+	}
+}
+
+// Server part
+
+// Runs gRPC server for GUI
+func (s *SnakeSimulator) StartServer() {
+	go func() {
+		grpcServer := grpc.NewServer()
+		RegisterSnakeSimulatorServer(grpcServer, s)
+		lis, err := net.Listen("tcp", "localhost:65002")
+		if err != nil {
+			fmt.Printf("Failed to listen: %v\n", err)
+		}
+
+		fmt.Printf("Listen SnakeSimulator localhost:65002\n")
+		if err := grpcServer.Serve(lis); err != nil {
+			fmt.Printf("Failed to serve: %v\n", err)
+		}
+	}()
+
+	go s.remoteControl.Run()
+}
+
+// Steaming of Field updates
+func (s *SnakeSimulator) Field(_ *None, srv SnakeSimulator_FieldServer) error {
+	ctx := srv.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		s.snakeReadMutex.Lock()
+		srv.Send(s.field)
+		s.snakeReadMutex.Unlock()
+		<-s.fieldUpdateQueue
+	}
+}
+
+// Steaming of Snake position and length updates
+func (s *SnakeSimulator) Snake(_ *None, srv SnakeSimulator_SnakeServer) error {
+	ctx := srv.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		srv.Send(s.snake)
+		<-s.snakeUpdateQueue
+	}
+}
+
+// Steaming of snake simulator statistic
+func (s *SnakeSimulator) Stats(_ *None, srv SnakeSimulator_StatsServer) error {
+	ctx := srv.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		s.fieldReadMutex.Lock()
+		srv.Send(s.stats)
+		s.fieldReadMutex.Unlock()
+		<-s.statsUpdateQueue
+	}
+}
+
+// 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
+}
+
+// Ask to play requested from gRPC GUI client
+func (s *SnakeSimulator) PlayBest(ctx context.Context, _ *None) (*None, error) {
+	s.isPlaying = true
+	s.isPlayingUpdateQueue <- s.isPlaying
+	return &None{}, nil
+}
+
+// Play in loop
+func (s *SnakeSimulator) PlayBestInLoop(_ context.Context, playBest *PlayingBestState) (*None, error) {
+	s.repeatInLoop = playBest.State
+	return &None{}, nil
+}
+
+// State of playing
+func (s *SnakeSimulator) IsPlaying(_ *None, srv SnakeSimulator_IsPlayingServer) error {
+	ctx := srv.Context()
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		srv.Send(&PlayingBestState{
+			State: s.isPlaying,
+		})
+		<-s.isPlayingUpdateQueue
+	}
+}

+ 78 - 0
snakesimulator/snakesimulator.proto

@@ -0,0 +1,78 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+ syntax="proto3";
+
+package snakesimulator;
+
+enum Direction {
+    Unknown = 0;
+    Up = 1;
+    Down = 2;
+    Left = 3;
+    Right = 4;
+}
+
+message Point {
+    uint32 x = 1;
+    uint32 y = 2;
+}
+
+message Snake {
+    repeated Point points = 1;
+}
+
+message Field {
+    uint32 width = 1;
+    uint32 height = 2;
+    Point food = 3;
+}
+
+message Stats {
+    uint32 generation = 1;
+    uint32 individual = 2;
+    uint32 move = 3;
+}
+
+message Speed {
+    uint32 speed = 1;
+}
+
+message PlayingBestState {
+    bool state = 1;
+}
+
+message None {
+}
+
+service SnakeSimulator {
+    rpc snake(None) returns (stream Snake) {}
+    rpc field(None) returns (stream Field) {}
+    rpc stats(None) returns (stream Stats) {}
+    rpc setSpeed(Speed) returns (None) {}
+    rpc playBest(None) returns (None) {}
+    rpc playBestInLoop(PlayingBestState) returns (None) {}
+    rpc isPlaying(None) returns (stream PlayingBestState) {}
+}

+ 23 - 0
snakesimulatorui/CMakeLists.txt

@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(SnakeSimulatorkUi LANGUAGES CXX)
+
+find_package(Qt5 COMPONENTS Quick Gui Core Qml REQUIRED)
+
+set(QTPROTOBUF_MAKE_TESTS false)
+set(QTPROTOBUF_MAKE_EXAMPLES false)
+add_subdirectory("qtprotobuf")
+find_package(QtProtobufProject CONFIG COMPONENTS QtProtobuf QtGrpc REQUIRED)
+if(Qt5_POSITION_INDEPENDENT_CODE)
+    set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)
+endif()
+
+file(GLOB PROTO_FILES ABSOLUTE "${CMAKE_CURRENT_SOURCE_DIR}/../snakesimulator/snakesimulator.proto")
+
+generate_qtprotobuf(TARGET SnakeSimulatorkUi PROTO_FILES ${PROTO_FILES} QML TRUE)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+add_executable(SnakeSimulatorkUi main.cpp qml.qrc clientwrapper.cpp)
+target_link_libraries(SnakeSimulatorkUi Qt5::Core Qt5::Gui Qt5::Qml Qt5::Quick QtProtobufProject::QtProtobuf QtProtobufProject::QtGrpc ${QtProtobuf_GENERATED})

+ 26 - 0
snakesimulatorui/clientwrapper.cpp

@@ -0,0 +1,26 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "clientwrapper.h"

+ 54 - 0
snakesimulatorui/clientwrapper.h

@@ -0,0 +1,54 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef CLIENTWRAPPER_H
+#define CLIENTWRAPPER_H
+
+#include <QObject>
+#include "snakesimulator_grpc.qpb.h"
+
+class ClientWrapper : public QObject
+{
+    Q_OBJECT
+public:
+    ClientWrapper(snakesimulator::SnakeSimulatorClient* client) :
+        m_client(client){}
+
+    Q_INVOKABLE void setSpeed(int speed) {
+        m_client->setSpeed({(QtProtobuf::uint32)speed, nullptr});
+    }
+
+    Q_INVOKABLE void playBest() {
+        m_client->playBest({nullptr});
+    }
+
+    Q_INVOKABLE void playBestInLoop(bool play) {
+        m_client->playBestInLoop({play, nullptr});
+    }
+private:
+    snakesimulator::SnakeSimulatorClient* m_client;
+};
+
+#endif // CLIENTWRAPPER_H

+ 79 - 0
snakesimulatorui/main.cpp

@@ -0,0 +1,79 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QDebug>
+#include <QtProtobufTypes>
+
+#include <QGrpcHttp2Channel>
+#include <QGrpcInsecureCredentials>
+#include "qtprotobuf_global.qpb.h"
+#include "snakesimulator_grpc.qpb.h"
+
+#include "clientwrapper.h"
+
+int main(int argc, char *argv[])
+{
+    QGuiApplication app(argc, argv);
+    QtProtobuf::qRegisterProtobufTypes();
+    snakesimulator::qRegisterProtobufTypes();
+
+    qmlRegisterUncreatableType<QtProtobuf::QGrpcAsyncReply>("snakesimulator", 1, 0, "QGrpcAsyncReply", "");
+    std::shared_ptr<snakesimulator::SnakeSimulatorClient> client(new snakesimulator::SnakeSimulatorClient);
+    auto chan = std::shared_ptr<QtProtobuf::QGrpcHttp2Channel>(new QtProtobuf::QGrpcHttp2Channel(QUrl("http://localhost:65002"), QtProtobuf::QGrpcInsecureCallCredentials()|QtProtobuf::QGrpcInsecureChannelCredentials()));
+    client->attachChannel(chan);
+
+    ClientWrapper *wrap = new ClientWrapper(client.get());
+
+    snakesimulator::Snake *snake = new snakesimulator::Snake;
+    QPointer<snakesimulator::Field> field = new snakesimulator::Field;
+    QPointer<snakesimulator::Stats> stats = new snakesimulator::Stats;
+    QPointer<snakesimulator::PlayingBestState> isPlaying = new snakesimulator::PlayingBestState;
+
+    client->subscribeFieldUpdates({}, field);
+    client->subscribeStatsUpdates({}, stats);
+    client->subscribeIsPlayingUpdates({}, isPlaying);
+
+    auto subscription = client->subscribeSnakeUpdates({});
+
+    QObject::connect(subscription, &QtProtobuf::QGrpcSubscription::updated, [subscription, snake](){
+        snake->setPoints(subscription->read<snakesimulator::Snake>().points()); //Issue https://github.com/semlanik/qtprotobuf/issues/48
+    });
+
+    QQmlApplicationEngine engine;
+    engine.rootContext()->setContextProperty("field", field);
+    engine.rootContext()->setContextProperty("snake", snake);
+    engine.rootContext()->setContextProperty("stats", stats);
+    engine.rootContext()->setContextProperty("client", wrap);
+    engine.rootContext()->setContextProperty("isPlaying", isPlaying);
+
+    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+    if (engine.rootObjects().isEmpty())
+        return -1;
+
+    return app.exec();
+}

+ 204 - 0
snakesimulatorui/main.qml

@@ -0,0 +1,204 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2019 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of NeuralNetwork project https://git.semlanik.org/semlanik/NeuralNetwork
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+import QtQuick 2.11
+import QtQuick.Window 2.11
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Styles 1.0
+
+import snakesimulator 1.0
+
+ApplicationWindow {
+    id: root
+    visible: true
+    property int tileSize: 20
+    width: field.width*tileSize + sideBar.width
+    height: field.height*tileSize
+
+    Rectangle {
+        color: "#565656"
+        anchors.fill: parent
+    }
+
+    Repeater {
+        model: snake.pointsData.length
+        Rectangle {
+            color: "#ffffff"
+            x: snake.pointsData[model.index].x*tileSize
+            y: snake.pointsData[model.index].y*tileSize
+            width: tileSize
+            height: tileSize
+        }
+    }
+
+    Rectangle {
+        color: "#99ee99"
+        x: field.food.x*tileSize
+        y: field.food.y*tileSize
+        width: tileSize
+        height: tileSize
+    }
+
+    Rectangle {
+        id: sideBar
+        width: speedControl.width + contentColumn.anchors.margins*2
+        color: "#000000"
+        anchors {
+            right: parent.right
+            top: parent.top
+            bottom: parent.bottom
+        }
+        Column {
+            id: contentColumn
+            anchors.fill: parent
+            anchors.margins: 10
+            spacing: 10
+            Text {
+                font.pointSize: 14
+                font.weight: Font.Bold
+                color: "#ffffff"
+                text: "Generation: " + stats.generation
+            }
+            Text {
+                font.pointSize: 14
+                font.weight: Font.Bold
+                color: "#ffffff"
+                text: "Individual: " + stats.individual
+            }
+            Text {
+                font.pointSize: 14
+                font.weight: Font.Bold
+                color: "#ffffff"
+                text: "Move: " + stats.move
+            }
+            Text {
+                font.pointSize: 14
+                font.weight: Font.Bold
+                color: "#ffffff"
+                text: "Speed: " + (speedControl.value > 10 ? "\u221e" : speedControl.value)
+            }
+            Slider {
+                id: speedControl
+                value: 5
+                from: 1
+                stepSize: 1.0
+                to: 11
+                snapMode: Slider.SnapAlways
+                onValueChanged: {
+                    changeTimer.restart()
+                }
+
+                background: Rectangle {
+                    x: speedControl.leftPadding
+                    y: speedControl.topPadding + speedControl.availableHeight / 2 - height / 2
+                    implicitWidth: 200
+                    implicitHeight: 4
+                    width: speedControl.availableWidth
+                    height: implicitHeight
+                    radius: 2
+                    color: "#bdbebf"
+
+                    Rectangle {
+                        x: 0
+                        width: speedControl.position * parent.width
+                        height: parent.height
+                        color: speedControl.value <= 10 ? "lightgreen" : "#003b6f"
+                        radius: 2
+                    }
+                }
+                Timer {
+                    id: changeTimer
+                    repeat: false
+                    interval: 200
+                    onTriggered: {
+                        client.setSpeed(speedControl.value > 10 ? 0 : speedControl.value)
+                    }
+                }
+            }
+        }
+
+
+        // it will play best in the loop afte naturel selection finished
+        CheckBox {
+            id: playBestCheckbox
+            anchors.margins: 10
+            anchors.bottom: bestBtn.top
+            anchors.right: parent.right
+            width: indicator.width + 170
+            checked: true
+            onCheckedChanged: {
+                client.playBestInLoop(playBestCheckbox.checked)
+            }
+
+            contentItem: Text {
+                id: text
+                color: "white"
+                text: "Repeat Best"
+                wrapMode: Text.NoWrap
+                anchors.left: playBestCheckbox.indicator.right
+                anchors.leftMargin: 10
+                anchors.right: undefined
+                verticalAlignment: Text.AlignVCenter
+            }
+
+            Component.onCompleted: {
+                client.playBestInLoop(playBestCheckbox.checked)
+            }
+        }
+
+        Button {
+            id: bestBtn
+            anchors.margins: 10
+            anchors.bottom: parent.bottom
+            anchors.right: parent.right
+
+            width: 100
+            height: 50
+
+            Rectangle {
+                anchors.fill: parent
+                enabled: isPlaying.state
+                color: isPlaying.state ? "#003b6f" : "lightgreen"
+            }
+
+            Text {
+                anchors.centerIn: parent
+                horizontalAlignment: Text.AlignVCenter
+                text: "Play best"
+            }
+
+            onClicked: {
+                client.playBest()
+            }
+        }
+    }
+
+    Connections {
+        target: field
+        onWidthChanged: {
+            console.log("New width: " + field.width)
+        }
+    }
+}

+ 5 - 0
snakesimulatorui/qml.qrc

@@ -0,0 +1,5 @@
+<RCC>
+    <qresource prefix="/">
+        <file>main.qml</file>
+    </qresource>
+</RCC>

+ 1 - 0
snakesimulatorui/qtprotobuf

@@ -0,0 +1 @@
+Subproject commit 6742f1167b945f2eee5032b997cf9250778a0e93