Kaynağa Gözat

Snake code cleanup

Alexey Edelev 5 yıl önce
ebeveyn
işleme
a6b089cfc9

+ 10 - 7
neuralnetwork/genetic/genetic.go

@@ -104,20 +104,23 @@ func crossbreed(firstParent, secondParent *neuralnetwork.NeuralNetwork, crossbre
 		secondParentWeights := secondParent.Weights[l]
 		firstParentBiases := firstParent.Biases[l]
 		secondParentBiases := secondParent.Biases[l]
+
 		r, c := firstParentWeights.Dims()
-		rp := int(float64(r) * crossbreedPart)
-		cp := int(float64(c) * crossbreedPart)
-		r = int(rand.Uint32())%(r-rp) + rp
-		c = int(rand.Uint32())%(c-cp) + cp
-		// for i := 0; i < int(float64(r)*crossbreedPart); i++ {
+
+		//Minimal part of matrix will be chosen for crossbreed
+		rWindow := int(float64(r) * crossbreedPart)
+		cWindow := int(float64(c) * crossbreedPart)
+
+		r = int(rand.Uint32())%(r-rWindow) + rWindow
+		c = int(rand.Uint32())%(c-cWindow) + cWindow
 		for i := 0; i < r; i++ {
 			for j := 0; j < c; j++ {
-				// Swap first half of weights
+				// Swap weights
 				w := firstParentWeights.At(i, j)
 				firstParentWeights.Set(i, j, secondParentWeights.At(i, j))
 				secondParentWeights.Set(i, j, w)
 			}
-			// Swap first half of biases
+			// Swap biases
 			b := firstParentBiases.At(i, 0)
 			firstParentBiases.Set(i, 0, secondParentBiases.At(i, 0))
 			secondParentBiases.Set(i, 0, b)

+ 9 - 7
neuralnetwork/genetic/mutagen/dummymutagen.go

@@ -8,23 +8,25 @@ import (
 )
 
 type DummyMutagen struct {
-	chance float64
+	chance        float64
+	mutationCount int
 }
 
-func NewDummyMutagen(chance float64) (rm *DummyMutagen) {
-	rm = &DummyMutagen{
-		chance: chance,
+func NewDummyMutagen(chance float64, mutationCount int) (dm *DummyMutagen) {
+	dm = &DummyMutagen{
+		chance:        chance,
+		mutationCount: mutationCount,
 	}
 	return
 }
 
-func (rm *DummyMutagen) Mutate(network *neuralnetwork.NeuralNetwork) {
+func (dm *DummyMutagen) Mutate(network *neuralnetwork.NeuralNetwork) {
 	rand.Seed(time.Now().UnixNano())
 	for l := 1; l < network.LayerCount; l++ {
 		randomized := rand.Float64()
-		if randomized < rm.chance {
+		if randomized < dm.chance {
 			r, c := network.Weights[l].Dims()
-			for o := 0; o < 1; o++ {
+			for o := 0; o < dm.mutationCount; o++ {
 				mutationRow := int(rand.Uint32()) % r
 				mutationColumn := int(rand.Uint32()) % c
 				weight := rand.NormFloat64()

+ 2 - 2
neuralnetwork/main.go

@@ -10,9 +10,9 @@ import (
 func main() {
 	rc := &remotecontrol.RemoteControl{}
 	go rc.Run()
-	s := snakesimulator.NewSnakeSimulator()
+	s := snakesimulator.NewSnakeSimulator(300)
 	s.StartServer()
-	p := genetic.NewPopulation(s, mutagen.NewDummyMutagen(50), genetic.PopulationConfig{PopulationSize: 2000, SelectionSize: 0.01, CrossbreedPart: 0.5}, []int{24, 18, 18, 4})
+	p := genetic.NewPopulation(s, mutagen.NewDummyMutagen(1.0, 1), genetic.PopulationConfig{PopulationSize: 2000, SelectionSize: 0.01, CrossbreedPart: 0.5}, []int{24, 18, 18, 4})
 	for _, net := range p.Networks {
 		net.SetStateWatcher(rc)
 	}

+ 66 - 3
neuralnetwork/snakesimulator/snake.go

@@ -8,6 +8,46 @@ func (p Point) Copy() (pCopy *Point) {
 	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 {
@@ -32,12 +72,35 @@ func (s *Snake) Feed(food *Point) {
 	s.Points = append([]*Point{food}, s.Points...)
 }
 
-func (s *Snake) SelfCollision(head *Point) int {
+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 {
-			return index
+			selfCollisionIndex = index
+			break
 		}
 	}
 
-	return 0
+	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
 }

+ 132 - 230
neuralnetwork/snakesimulator/snakesimulator.go

@@ -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