Full game
@include("post.shs")
@wire(pulse-bg { ;; flashes the sreen
{Take("duration") = pulse-dur}
{Take("color") = pulse-color}
Animation.Timer(Duration: pulse-dur Action: {
original-color > terminal-bg-color
Stop
}) | Div(pulse-dur)
Lerp(First: pulse-color Second: original-color) > terminal-bg-color
} Looped: true)
@wire(snake-game-logic {
;; move snake
Inputs.KeyDown("right" {
0 | Update(input-state "direction")
})
Inputs.KeyDown("down" {
1 | Update(input-state "direction")
})
Inputs.KeyDown("left" {
2 | Update(input-state "direction")
})
Inputs.KeyDown("up" {
3 | Update(input-state "direction")
})
snake-direction >= prev-dir
input-state | Take("direction")
Match([ ;; takes what was given from input
0 {; right
prev-dir:0 | When(Is(0) { ;; prev-dir:0 is taking the x
@i2(1 0) > snake-direction ;; if player pressed right and snake was previously not moving left or right, then change direction to move right
})
}
1 {; down
prev-dir:1 | When(Is(0) { ;;
@i2(0 1) > snake-direction ;; if player pressed down and snake was previously not moving up or down, then change direction to move down
})
}
2 {; left
prev-dir:0 | When(Is(0) {
@i2(-1 0) > snake-direction ;; if player pressed left and snake was previously not moving left or right, then change direction to move left
})
}
3 {; up
prev-dir:1 | When(Is(0) {
@i2(0 -1) > snake-direction ;; if player pressed down and snake was previously not moving up or down, then change direction to move down
})
}
])
Animation.Timer(
Duration: move-duration
Looped: true
Action: {
snake-segments | RTake(0) ;; take the last element in the sequence which is the snake head
Math.Add(snake-direction) = new-snake-head
new-snake-head >> snake-segments
;; food collision logic
food-pos
If(Predicate: {
IsNotNone
And
new-snake-head | Is((food-pos | ToInt2))
} Then: {
none > food-pos
score | Math.Add(1) > score
move-duration | Mul(0.95) > move-duration
{duration: 0.35 color: @f4(1.0 1.0 1.0 1.0)}
Detach(pulse-bg)
} Else: {
DropFront(snake-segments)
})
;; wall collision logic
(new-snake-head | Take(0))
When(Predicate: {
IsMoreEqual(grid-width)
Or
(new-snake-head | Take(1)) | IsMoreEqual(grid-length)
Or
(new-snake-head | Take(0)) | IsLess(0)
Or
(new-snake-head | Take(1)) | IsLess(0)
Or
snake-segments | Slice(From: 0 To: (Count(snake-segments) | Math.Subtract(2))) | IsAny(new-snake-head)
} Action: {
true > game-over
{duration: 0.20 color: @f4(1.0 0.0 0.0 1.0)}
Detach(pulse-bg)
})
}
)
food-pos
When(Predicate: Is(none) Action: {
; Spawn new food if needed
RandomInt(grid-width) = food-x
RandomInt(grid-length) = food-y
@i2(food-x food-y) > food-pos
})
} Looped: true)
@wire(main-wire {
GFX.MainWindow(
Contents: {
Once({
; Create render steps
GFX.BuiltinFeature(BuiltinFeatureId::Transform) >> features
GFX.BuiltinFeature(BuiltinFeatureId::BaseColor) >> features
GFX.BuiltinFeature(BuiltinFeatureId::AlphaBlend) >> features
GFX.DrawQueue = queue
40 >= font-size
@read("./snake/Px437_IBM_EGA_8x8.ttf" Bytes: true) | GFX.FontMap = font ;; change the file path to where your font ttf file is stored
[@i2(10 10) @i2(11 10) @i2(12 10)] >= snake-segments
@i2(1 0) >= snake-direction
{direction: 0} >= input-state
0.15 >= move-duration
none | ToAny >= food-pos
0 >= score
false >= game-over
@f4(0.0 0.0 0.0 1.0) >= original-color
original-color | Set(Name:terminal-bg-color Global: true)
})
GFX.DrawablePass(Features: features Queue: queue) >> render-steps
Do(post-fx)
GFX.Viewport = vp ;; gets the screen's left, top, right, btm coordinates
[vp font-size] | Memoize({
vp:2 | Sub((vp:0)) | ToFloat = width ;; screen width
vp:3 | Sub((vp:1)) | ToFloat = height ;; screen height
@f2(width height) | Math.Multiply(@f2(-0.5 -0.5)) >= view-offset ;; makes the grid start from the top left of the screen
font | GFX.FontSpaceSize(font-size) | ToFloat2 = font-cell-size
font-cell-size | Take(0) = font-size-width
font-cell-size | Take(1) = font-size-height
width | Math.Mod((font-size-width)) | Math.Multiply(0.5) = offset-x
height | Math.Mod((font-size-height)) | Math.Multiply(0.5) = offset-y
width | Div((font-size-width | ToFloat)) | ToInt | Max(1) = grid-width ;; dividing the screen into a grid based on font.
height | Div((font-size-height | ToFloat)) | ToInt | Max(1) = grid-length ;; Max ensures at least 1 cell in the grid
@f2(offset-x offset-y) | Add(view-offset) > view-offset
view-offset | ToFloat3 | Math.Translation = view-transform ;; final view transform
})
GFX.View(View: view-transform OrthographicSize: @f2(1.0 -1.0) OrthographicSizeType: OrthographicSizeType::PixelScale) = view
;; game logic
game-over
If(Predicate: Is(false) Then: {
Step(snake-game-logic)
} Else: {
Inputs.KeyDown(Key: "r" Action: {
false > game-over
0 > score
[@i2(10 10) @i2(11 10) @i2(12 10)] > snake-segments
0.15 > move-duration
none > food-pos
@i2(1 0) > snake-direction
{direction: 0} > input-state
})
})
;; draw game
GFX.DynMesh = dmesh
0 >= repeat-idx
snake-segments ;; snake body segments
Repeat(
Action: {
snake-segments | Take(repeat-idx)
= coord
"█" | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (coord | ToFloat3 | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(0.5 0.8 0.5 1.0)
VAlign: 1.0
)
repeat-idx | Math.Add(1) > repeat-idx
}
Until: {
repeat-idx | Is((Count(snake-segments) | Math.Subtract(1))) ;; repeat only until the second last segment
}
)
snake-segments | RTake(0) ;; snake head
= head-coord
"█" | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (head-coord | ToFloat3 | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(0.1 1.0 0.1 1.0)
VAlign: 1.0
)
;; draw food
food-pos | When(Predicate: IsNotNone Action: {
"○" | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (food-pos | ToFloat3 | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(1.0 0.2 0.2 1.0) ;; red color
VAlign: 1.0
)
})
;; display score
["Score: " score] | String.Format | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (@f3(1.0 1.0 0.0) | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(1.0 1.0 1.0 1.0) ;; white
VAlign: 1.0
)
"Game Over" = game-over-string
(grid-width | Math.Divide(2)) | ToFloat = grid-width-center
(grid-length| Math.Divide(2)) | ToFloat = grid-length-center
Count(game-over-string) | Math.Divide(2) | ToFloat = str-center
"press r to reset" = reset-string
Count(reset-string) | Math.Divide(2) | ToFloat = reset-str-center
;; display game over
game-over
When(Predicate: Is(true) Action: {
game-over-string | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (@f3((grid-width-center | Math.Subtract(str-center)) grid-length-center 0.0) | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(1.0 1.0 1.0 1.0) ;; white
VAlign: 1.0
)
reset-string | GFX.DynDrawText(
Font: font
FontSize: font-size
Output: dmesh
Offset: (@f3((grid-width-center | Math.Subtract(reset-str-center)) (grid-length-center | Math.Add(1.0)) 0.0) | Math.Multiply((font-cell-size | ToFloat3)))
Scale: 1.0
Color: @f4(1.0 1.0 1.0 1.0) ;; white
VAlign: 1.0
)
})
dmesh | GFX.DynToMesh | DoMany({
{Take("mesh") = mesh}
{Take("texture") = texture}
Math.MatIdentity | GFX.Drawable(mesh Params: {baseColorTexture: texture}) | GFX.Draw(queue)
} ComposeSync: true)
GFX.Render(render-steps view)
})
} Looped: true)
@mesh(main)
@schedule(main main-wire)
@run(main FPS: 60)