Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
david-littlefarmer committed Dec 27, 2023
1 parent 7f500ab commit ea3c17c
Show file tree
Hide file tree
Showing 12 changed files with 735 additions and 554 deletions.
53 changes: 36 additions & 17 deletions ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,49 @@ import (
"time"

"github.com/fzipp/astar"

"github.com/golang-cz/snake/proto"
)

func (s *Server) createFood() {
s.mu.Lock()
defer s.mu.Unlock()

s.state.Items[s.lastItemId] = &proto.Item{
bite := proto.ItemType_bite
item := &proto.Item{
Id: s.lastItemId,
Color: "red",
Body: &proto.Square{
Coordinate: &proto.Coordinate{
X: uint(rand.Intn(int(s.state.Width))),
Y: uint(rand.Intn(int(s.state.Height))),
},
Type: &bite,
}

s.state.Items[s.lastItemId] = item
s.lastItemId++

u := &proto.Update{
Diffs: []*proto.Diff{
{
X: item.Coordinate.X,
Y: item.Coordinate.Y,
Color: item.Color,
Add: true,
},
},
}

s.sendUpdate(u)
}

func (s *Server) generateFood() {
s.createFood()
s.createFood()
s.createFood()
for i := 0; i < NumOfStartingFood; i++ {
s.createFood()
}

for {
<-time.After(2 * time.Second)
<-time.After(FoodGenerateInterval)
s.createFood()
}
}
Expand All @@ -43,7 +62,7 @@ func (s *Server) currentGrid() grid {
}

for _, item := range s.state.Items {
grid.put(image.Point{X: int(item.Body.X), Y: int(item.Body.Y)}, '*')
grid.put(image.Point{X: int(item.Coordinate.X), Y: int(item.Coordinate.Y)}, '*')
}

for _, snake := range s.state.Snakes {
Expand All @@ -58,7 +77,7 @@ func (s *Server) currentGrid() grid {
func (s *Server) generateSnakeTurns(grid grid) {
// Turn "AI" snakes to the closes food using A* algorithm.
for _, snake := range s.state.Snakes {
if snake.Name != "AI" {
if !strings.Contains(snake.Name, "AI") {
continue
}

Expand All @@ -75,7 +94,7 @@ func (s *Server) generateSnakeTurns(grid grid) {
shortestPathLen := math.MaxInt

for _, item := range s.state.Items {
food := squareToPoint(item.Body)
food := squareToPoint(item.Coordinate)

path := astar.FindPath[image.Point](grid, snakeHead, food, distance, distance)
if len(path) > 1 && len(path) < shortestPathLen {
Expand All @@ -97,20 +116,20 @@ func (s *Server) generateSnakeTurns(grid grid) {

switch {
case snakeHead.X < nextSquare.X:
if turnSnake(snake, &right, 0) == proto.ErrTurnAbout {
turnSnake(snake, &up, 0)
if turnSnake(snake, &Right, 0) == proto.ErrTurnAbout {
turnSnake(snake, &Up, 0)
}
case snakeHead.X > nextSquare.X:
if turnSnake(snake, &left, 0) == proto.ErrTurnAbout {
turnSnake(snake, &down, 0)
if turnSnake(snake, &Left, 0) == proto.ErrTurnAbout {
turnSnake(snake, &Down, 0)
}
case snakeHead.Y < nextSquare.Y:
if turnSnake(snake, &down, 0) == proto.ErrTurnAbout {
turnSnake(snake, &left, 0)
if turnSnake(snake, &Down, 0) == proto.ErrTurnAbout {
turnSnake(snake, &Left, 0)
}
case snakeHead.Y > nextSquare.Y:
if turnSnake(snake, &up, 0) == proto.ErrTurnAbout {
turnSnake(snake, &right, 0)
if turnSnake(snake, &Up, 0) == proto.ErrTurnAbout {
turnSnake(snake, &Right, 0)
}
}
}
Expand Down
129 changes: 95 additions & 34 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,38 @@ import (
"context"
"fmt"
"image"
"strings"
"time"

"github.com/golang-cz/snake/proto"
)

func (s *Server) Run(ctx context.Context) error {
go s.generateFood()
go s.generateAISnakes()

for i := 0; i < 3; i++ {
s.createSnake("AI")
ticker := time.NewTicker(GameTickTime)
defer ticker.Stop()

for range ticker.C {
// if err := s.gameTick(); err != nil {
// return fmt.Errorf("advancing the game: %w", err)
// }

s.gameTick()
}

ticker := time.NewTicker(75 * time.Millisecond)
defer ticker.Stop()
return nil
}

for {
select {
case <-ticker.C:
if err := s.gameTick(); err != nil {
return fmt.Errorf("advancing the game: %w", err)
}
}
func (s *Server) generateAISnakes() {
for i := 0; i < NumOfAISnakes; i++ {
time.Sleep(time.Second)
go s.createSnake(fmt.Sprintf("AI%v", i))
}
}

func (s *Server) gameTick() error {
func (s *Server) gameTick() {
s.mu.Lock()
defer s.mu.Unlock()

Expand All @@ -38,9 +44,13 @@ func (s *Server) gameTick() error {

s.generateSnakeTurns(grid)

u := &proto.Update{
Diffs: []*proto.Diff{},
}

// Move snakes.
for _, snake := range s.state.Snakes {
next := &proto.Square{}
next := &proto.Coordinate{}

if len(snake.NextDirections) > 0 {
snake.Direction = snake.NextDirections[0]
Expand Down Expand Up @@ -69,66 +79,117 @@ func (s *Server) gameTick() error {
// Move snake's head.
grid.put(nextPoint, snakeRune(snake.Id))

snake.Body = append([]*proto.Square{next}, snake.Body...)
snake.Body = append([]*proto.Coordinate{next}, snake.Body...)

// Remove food.
for _, item := range s.state.Items {
if item.Body.X == next.X && item.Body.Y == next.Y {
if item.Coordinate.X == next.X && item.Coordinate.Y == next.Y {
delete(s.state.Items, item.Id)
}
}

diff := &proto.Diff{
X: next.X,
Y: next.Y,
Color: snake.Color,
Add: true,
}

u.Diffs = append(u.Diffs, diff)

snake.Length = len(snake.Body)

case ' ', '.', snakeRune(snake.Id):
// Move snake's head & remove tail.
tailPoint := squareToPoint(snake.Body[len(snake.Body)-1])
tail := snake.Body[len(snake.Body)-1]
tailPoint := squareToPoint(tail)
grid.put(tailPoint, ' ')
grid.put(nextPoint, snakeRune(snake.Id))

snake.Body = append([]*proto.Square{next}, snake.Body[:len(snake.Body)-1]...)
headDiff := &proto.Diff{
X: next.X,
Y: next.Y,
Color: snake.Color,
Add: true,
}

tailDiff := &proto.Diff{
X: tail.X,
Y: tail.Y,
Add: false,
}

u.Diffs = append(u.Diffs, headDiff)
u.Diffs = append(u.Diffs, tailDiff)

snake.Body = append([]*proto.Coordinate{next}, snake.Body[:len(snake.Body)-1]...)

default:
// Crashed into another snake.
name := snake.Name
delete(s.state.Snakes, snake.Id)

// Create food from snake's body.
for i, square := range snake.Body {
if i%2 == 0 {
s.state.Items[s.lastItemId] = &proto.Item{
Id: s.lastItemId,
Color: "red",
Body: square,
bite := proto.ItemType_bite
for i, bodyPart := range snake.Body {
diff := &proto.Diff{
X: bodyPart.X,
Y: bodyPart.Y,
}

// Create food from snake's body.
if i%FoodFromDeadSnake == 0 {
item := &proto.Item{
Id: s.lastItemId,
Color: "red",
Coordinate: bodyPart,
Type: &bite,
}

s.state.Items[s.lastItemId] = item
s.lastItemId++

diff.Color = item.Color
diff.Add = true
}

u.Diffs = append(u.Diffs, diff)
}

// Reborn AI.
if snake.Name == "AI" {
if strings.Contains(snake.Name, "AI") {
go func() {
<-time.After(10 * time.Second)
s.createSnake("AI")
<-time.After(AISnakeRespawnTime)
s.createSnake(name)
}()
}
}
}

// Generate bite, if snakes ate all bites
bites := 0
for _, item := range s.state.Items {
if item.Type != nil && *item.Type == proto.ItemType_bite {
bites++
}
}

return s.sendState(s.state)
if bites == 0 {
go s.createFood()
}

s.sendUpdate(u)
}

// TODO: We send the whole state on each update. Optimize to send events (diffs) only.
func (s *Server) sendState(state *proto.State) error {
func (s *Server) sendUpdate(state *proto.Update) {
for _, sub := range s.subs {
sub := sub
go func() {
sub <- state
}()
}
return nil
}

var runes = []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}

// Get letter from A to Z
func snakeRune(snakeId uint64) rune {
return runes[int(snakeId)%len(runes)]
return rune((snakeId % 26) + 65)
}
19 changes: 18 additions & 1 deletion grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package main
import (
"image"
"math"
"os"
"os/exec"
"runtime"
"strings"

"github.com/golang-cz/snake/proto"
Expand All @@ -11,6 +14,7 @@ import (
type grid []string

func (g grid) String() string {
clearTerminal()
var b strings.Builder
b.WriteByte('\n')
b.WriteString(strings.Repeat("▮", len(g)+2))
Expand Down Expand Up @@ -65,9 +69,22 @@ func distance(p, q image.Point) float64 {
return math.Sqrt(float64(d.X*d.X + d.Y*d.Y))
}

func squareToPoint(s *proto.Square) image.Point {
func squareToPoint(s *proto.Coordinate) image.Point {
return image.Point{
X: int(s.X),
Y: int(s.Y),
}
}

func clearTerminal() {
var cmd *exec.Cmd

if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", "cls")
} else {
cmd = exec.Command("clear")
}

cmd.Stdout = os.Stdout
cmd.Run()
}
Loading

0 comments on commit ea3c17c

Please sign in to comment.