plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
6203667
— Peter John
2026-11-04T13:09:59+05:30
examples: consolidate 39 files into 25 topical ones (tests unchanged: 408); assets and tools move under games/
- plum-examples/annotated_locals.plum +0 -78
- plum-examples/{struct_arrays.plum → arrays.plum} +54 -6
- plum-examples/basics.plum +115 -2
- plum-examples/cons_list.plum +0 -70
- plum-examples/constants.plum +0 -72
- plum-examples/control_flow.plum +133 -1
- plum-examples/digit_names.plum +0 -59
- plum-examples/{error_propagation.plum → errors.plum} +238 -0
- plum-examples/forms.plum +0 -155
- plum-examples/functions.plum +0 -77
- plum-examples/{assets → games/assets}/gameover.wav +0 -0
- plum-examples/{assets → games/assets}/hit.wav +0 -0
- plum-examples/{assets → games/assets}/music.wav +0 -0
- plum-examples/{assets → games/assets}/pickup.wav +0 -0
- plum-examples/{assets → games/assets}/sprites.png +0 -0
- plum-examples/games/functional/game_state_view.plum +450 -549
- plum-examples/games/imperative/game.plum +43 -23
- plum-examples/{game_loop.plum → games/imperative/game_loop.plum} +1 -1
- plum-examples/games/imperative/game_scene.plum +51 -47
- plum-examples/games/imperative/scene.plum +7 -8
- plum-examples/games/pure/game_pure_scene.plum +181 -163
- plum-examples/{tools → games/tools}/make_sounds.py +1 -1
- plum-examples/{tools → games/tools}/make_sprites.py +0 -0
- plum-examples/io.plum +0 -14
- plum-examples/loops.plum +0 -125
- plum-examples/numbers.plum +142 -0
- plum-examples/operators.plum +0 -67
- plum-examples/optionals.plum +0 -232
- plum-examples/own_list.plum +0 -33
- plum-examples/recursion.plum +221 -0
- plum-examples/{seq.plum → sequences.plum} +161 -2
- plum-examples/struct_union_members.plum +1 -1
- plum-examples/structs_records.plum +156 -12
- plum-examples/tail_calls.plum +0 -110
- plum-examples/testing.plum +0 -20
- plum-examples/trait_scene.plum +0 -50
- plum-examples/traits.plum +62 -6
- plum-examples/typed_arrays.plum +0 -39
- plum-examples/unboxed_variants.plum +6 -6
plum-examples/annotated_locals.plum
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# Annotated local declarations: `name: Type := value` fixes the local's type to
|
|
2
|
-
# the annotation. The value converts to it exactly as it would for a parameter of
|
|
3
|
-
# that type (a union member into its enum, `T` into `T?`, `null` into `T?`), and
|
|
4
|
-
# a later `name = value` is checked and converted against the same type.
|
|
5
|
-
module annotated_locals
|
|
6
|
-
import std/Str
|
|
7
|
-
import std/Int
|
|
8
|
-
import std/Float
|
|
9
|
-
import std/Slice
|
|
10
|
-
|
|
11
|
-
record Circle(r: Float)
|
|
12
|
-
|
|
13
|
-
record Rect(w: Float, h: Float)
|
|
14
|
-
|
|
15
|
-
enum Shape = Circle | Rect
|
|
16
|
-
|
|
17
|
-
error LocalError = Bad | Worse
|
|
18
|
-
|
|
19
|
-
fun area(s: Shape) -> Float =
|
|
20
|
-
match s
|
|
21
|
-
Circle(r) => r * r * 3.0
|
|
22
|
-
Rect(w, h) => w * h
|
|
23
|
-
|
|
24
|
-
fun parseLocal(n: Int) -> LocalError!Int =
|
|
25
|
-
if n < 0
|
|
26
|
-
return error.Bad
|
|
27
|
-
n * 2
|
|
28
|
-
|
|
29
|
-
fun twice(x: Int) -> Int = x * 2
|
|
30
|
-
|
|
31
|
-
fun same(x: T) -> T =
|
|
32
|
-
y: T := x
|
|
33
|
-
y
|
|
34
|
-
|
|
35
|
-
test "union_local"
|
|
36
|
-
s: Shape := Circle(r: 2.0)
|
|
37
|
-
assert area(s) == 12.0
|
|
38
|
-
|
|
39
|
-
test "union_local_reassigned"
|
|
40
|
-
s: Shape := Circle(r: 1.0)
|
|
41
|
-
s = Rect(w: 2.0, h: 3.0)
|
|
42
|
-
assert area(s) == 6.0
|
|
43
|
-
|
|
44
|
-
test "optional_local_present"
|
|
45
|
-
o: Int? := 4
|
|
46
|
-
assert {o ?: 0} == 4
|
|
47
|
-
|
|
48
|
-
test "optional_local_null"
|
|
49
|
-
o: Int? := null
|
|
50
|
-
assert {o ?: 9} == 9
|
|
51
|
-
o = 5
|
|
52
|
-
assert {o ?: 9} == 5
|
|
53
|
-
|
|
54
|
-
test "list_local"
|
|
55
|
-
xs: Slice[Shape] := Slice[Shape]()
|
|
56
|
-
xs.push(Circle(r: 1.0))
|
|
57
|
-
xs.push(Rect(w: 1.0, h: 1.0))
|
|
58
|
-
assert xs.length() == 2
|
|
59
|
-
|
|
60
|
-
test "fn_local"
|
|
61
|
-
f: fn(Int) -> Int := twice
|
|
62
|
-
assert f(4) == 8
|
|
63
|
-
|
|
64
|
-
test "scalar_locals"
|
|
65
|
-
n: Int := 3
|
|
66
|
-
x: Float := 2.5
|
|
67
|
-
assert n + 1 == 4
|
|
68
|
-
assert x * 2.0 == 5.0
|
|
69
|
-
|
|
70
|
-
test "generic_param_annotation"
|
|
71
|
-
assert same(7) == 7
|
|
72
|
-
assert same("a") == "a"
|
|
73
|
-
|
|
74
|
-
test "named_error_set_local"
|
|
75
|
-
ok: LocalError!Int := parseLocal(4)
|
|
76
|
-
bad: LocalError!Int := parseLocal(-1)
|
|
77
|
-
assert {ok catch 0} == 8
|
|
78
|
-
assert {bad catch 7} == 7
|
plum-examples/{struct_arrays.plum → arrays.plum}
RENAMED
|
@@ -1,13 +1,48 @@
|
|
|
1
|
-
# Arrays
|
|
1
|
+
# Arrays: typed native arrays and arrays of structs.
|
|
2
|
-
# is one object holding an `f64` array for `x` and one for `y`, so `get`/`set`
|
|
3
|
-
# allocate nothing per element. `Slice`, `Grid` and friends get this for free.
|
|
4
|
-
module struct_arrays
|
|
5
|
-
import std/Str
|
|
6
2
|
import std/Int
|
|
7
3
|
import std/Float
|
|
8
4
|
import std/Array
|
|
9
|
-
import std/Slice
|
|
10
5
|
import std/Grid
|
|
6
|
+
import std/Seq
|
|
7
|
+
import std/Str
|
|
8
|
+
import std/Slice
|
|
9
|
+
|
|
10
|
+
# ==== typed arrays ====
|
|
11
|
+
|
|
12
|
+
# Typed arrays: unboxed native `Array[Int]` and `Array[Float]` lowering to
|
|
13
|
+
# WebAssembly primitive arrays, along with flat 2D `Grid[T]`.
|
|
14
|
+
|
|
15
|
+
# Tile-grid style data: Array[Int] / Array[Float] / Array[Bool] are native
|
|
16
|
+
# unboxed wasm arrays (i64 / f64 / i32 elements), not boxed anyref arrays.
|
|
17
|
+
fun sumInts(n: Int) -> Int =
|
|
18
|
+
a := Array.filled(n, 3)
|
|
19
|
+
total := 0
|
|
20
|
+
for i := range n
|
|
21
|
+
total = total + a.get(i)
|
|
22
|
+
return total
|
|
23
|
+
|
|
24
|
+
fun sumFloats(n: Int) -> Float =
|
|
25
|
+
a := Array.filled(n, 0.5)
|
|
26
|
+
total := 0.0
|
|
27
|
+
for i := range n
|
|
28
|
+
total = total + a.get(i)
|
|
29
|
+
return total
|
|
30
|
+
|
|
31
|
+
fun countTrue(n: Int) -> Int =
|
|
32
|
+
a := Array.filled(n, false)
|
|
33
|
+
a.set(1, true)
|
|
34
|
+
a.set(3, true)
|
|
35
|
+
c := 0
|
|
36
|
+
for i := range n
|
|
37
|
+
if a.get(i)
|
|
38
|
+
c = c + 1
|
|
39
|
+
return c
|
|
40
|
+
|
|
41
|
+
# ==== struct arrays ====
|
|
42
|
+
|
|
43
|
+
# Arrays of structs are stored field by field (struct-of-arrays): `Array[Vec2]`
|
|
44
|
+
# is one object holding an `f64` array for `x` and one for `y`, so `get`/`set`
|
|
45
|
+
# allocate nothing per element. `Slice`, `Grid` and friends get this for free.
|
|
11
46
|
|
|
12
47
|
struct Vec2(x: Float, y: Float)
|
|
13
48
|
|
|
@@ -21,9 +56,13 @@ struct Tile(id: Int, solid: Bool, weight: Float)
|
|
|
21
56
|
record World(points: Slice[Vec2], name: Str)
|
|
22
57
|
|
|
23
58
|
struct One(n: Int)
|
|
59
|
+
|
|
24
60
|
struct Flags(a: Bool, b: Bool, c: Int)
|
|
61
|
+
|
|
25
62
|
struct L3(r: Rect, k: Int)
|
|
63
|
+
|
|
26
64
|
record Holder(arr: Array[Vec2])
|
|
65
|
+
|
|
27
66
|
record Named(label: Str, v: Vec2)
|
|
28
67
|
|
|
29
68
|
fun bump(h: Holder) =
|
|
@@ -40,6 +79,15 @@ struct Px(r: Byte, g: Byte, b: Byte, a: Byte)
|
|
|
40
79
|
fun mkPx(v: Int) -> Px =
|
|
41
80
|
Px(r: Byte(v), g: Byte(v + 1), b: Byte(v + 2), a: Byte(255))
|
|
42
81
|
|
|
82
|
+
# ==== typed arrays tests ====
|
|
83
|
+
|
|
84
|
+
test "array_int_native: filled arrays hold their values"
|
|
85
|
+
assert sumInts(10) == 30
|
|
86
|
+
assert sumFloats(4) == 2.0
|
|
87
|
+
assert countTrue(5) == 2
|
|
88
|
+
|
|
89
|
+
# ==== struct arrays tests ====
|
|
90
|
+
|
|
43
91
|
test "List[Vec2] push/get/set round-trips across growth"
|
|
44
92
|
l := Slice[Vec2]()
|
|
45
93
|
for i := range 100
|
plum-examples/basics.plum
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
# Basics: literals, operators, functions, testing and the Io entry point.
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
+
import std/Io
|
|
6
|
+
|
|
7
|
+
# ==== basics ====
|
|
5
8
|
|
|
6
9
|
# A `primitive` with a non-built-in name is a stateless namespace: constants and
|
|
7
10
|
# static functions, reached as `Config.MaxRetries`. It is not a value or a type.
|
|
@@ -10,7 +13,7 @@ primitive Config =
|
|
|
10
13
|
GoldenRatio = 1.61803
|
|
11
14
|
Greeting = "hello"
|
|
12
15
|
|
|
13
|
-
fun
|
|
16
|
+
fun syntaxTour() =
|
|
14
17
|
dec := 42
|
|
15
18
|
hex := 0xFF
|
|
16
19
|
bin := 0b1010
|
|
@@ -31,3 +34,113 @@ fun main() =
|
|
|
31
34
|
positive := +sum
|
|
32
35
|
inverted := !cmp
|
|
33
36
|
a, b := 1, 2
|
|
37
|
+
|
|
38
|
+
# ==== io ====
|
|
39
|
+
|
|
40
|
+
# Every call that leaves the program goes through the `Io` capability. An entry
|
|
41
|
+
# point (`main`, an `export fun`, a `test`) may take it as its first parameter.
|
|
42
|
+
fun main(io: Io) =
|
|
43
|
+
io.printLn("hello from plum")
|
|
44
|
+
name := "world"
|
|
45
|
+
io.printLn("hello, {name}!")
|
|
46
|
+
|
|
47
|
+
# ==== testing ====
|
|
48
|
+
|
|
49
|
+
# Testing: native `test "name"` blocks and `assert` statements executed by
|
|
50
|
+
# the `plum test` runner.
|
|
51
|
+
|
|
52
|
+
fun add(a: Int, b: Int) -> Int =
|
|
53
|
+
a + b
|
|
54
|
+
|
|
55
|
+
fun isEven(n: Int) -> Bool =
|
|
56
|
+
n % 2 == 0
|
|
57
|
+
|
|
58
|
+
# ==== functions ====
|
|
59
|
+
|
|
60
|
+
# Functions: top-level function declarations, typed arguments, default and
|
|
61
|
+
# variadic parameters, return expressions, and recursion.
|
|
62
|
+
|
|
63
|
+
fun addInts(a: Int, b: Int) -> Int =
|
|
64
|
+
a + b
|
|
65
|
+
|
|
66
|
+
fun greet() =
|
|
67
|
+
todo
|
|
68
|
+
|
|
69
|
+
fun withDefault(a: Int, step: Int = 1) -> Int =
|
|
70
|
+
a + step
|
|
71
|
+
|
|
72
|
+
fun sumAll(nums: ...Int) -> Int =
|
|
73
|
+
total := 0
|
|
74
|
+
for v := range nums
|
|
75
|
+
total = total + v
|
|
76
|
+
total
|
|
77
|
+
|
|
78
|
+
fun wrap(value: T) -> Bool =
|
|
79
|
+
true
|
|
80
|
+
|
|
81
|
+
fun pair(first: T, second: U) -> Bool =
|
|
82
|
+
true
|
|
83
|
+
|
|
84
|
+
fun useWrap() -> Bool =
|
|
85
|
+
wrap(5)
|
|
86
|
+
|
|
87
|
+
fun usePair() -> Bool =
|
|
88
|
+
pair(1, "x")
|
|
89
|
+
|
|
90
|
+
# ---- recursion, defaults and variadics ----
|
|
91
|
+
|
|
92
|
+
fun factorial(x: Int) -> Int =
|
|
93
|
+
if x < 2
|
|
94
|
+
return 1
|
|
95
|
+
return x * factorial(x - 1)
|
|
96
|
+
|
|
97
|
+
# Regression test: `double(...)` (an all-lowercase, no-uppercase, no-underscore
|
|
98
|
+
# callee) used to fail to parse at all — `var_identifier` and `fn_identifier`
|
|
99
|
+
# both matched its text and the grammar's lexer would nondeterministically
|
|
100
|
+
# commit to `var_identifier`, breaking every such call site.
|
|
101
|
+
fun double(n: Int) -> Int =
|
|
102
|
+
n * 2
|
|
103
|
+
|
|
104
|
+
fun combineVariadic(prefix: Int, rest: ...Int) -> Int =
|
|
105
|
+
prefix
|
|
106
|
+
|
|
107
|
+
fun weighted(nums: ...Int) -> Int =
|
|
108
|
+
total := 0
|
|
109
|
+
for i, v := range nums
|
|
110
|
+
total = total + i * v
|
|
111
|
+
total
|
|
112
|
+
|
|
113
|
+
# ==== testing tests ====
|
|
114
|
+
|
|
115
|
+
test "add works"
|
|
116
|
+
assert add(1, 2) == 3
|
|
117
|
+
assert add(2, 2) == 4
|
|
118
|
+
|
|
119
|
+
test "isEven works"
|
|
120
|
+
assert isEven(4)
|
|
121
|
+
assert !isEven(3)
|
|
122
|
+
assert isEven(2)
|
|
123
|
+
|
|
124
|
+
# ==== functions tests ====
|
|
125
|
+
|
|
126
|
+
test "factorial runs correctly"
|
|
127
|
+
assert factorial(5) == 120
|
|
128
|
+
|
|
129
|
+
test "lowercase single word function call runs correctly"
|
|
130
|
+
assert double(21) == 42
|
|
131
|
+
|
|
132
|
+
test "variadic call with varying trailing arg counts runs correctly"
|
|
133
|
+
a := combineVariadic(10)
|
|
134
|
+
b := combineVariadic(20, 1)
|
|
135
|
+
c := combineVariadic(30, 1, 2, 3)
|
|
136
|
+
assert a + b + c == 60
|
|
137
|
+
|
|
138
|
+
test "sum all variadic int runs correctly"
|
|
139
|
+
assert sumAll(1, 2, 3, 4) == 10
|
|
140
|
+
|
|
141
|
+
test "sum all variadic int with zero args runs correctly"
|
|
142
|
+
assert sumAll() == 0
|
|
143
|
+
|
|
144
|
+
test "for over a variadic binds index and element"
|
|
145
|
+
assert weighted() == 0
|
|
146
|
+
assert weighted(5, 10, 20) == 50
|
plum-examples/cons_list.plum
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# The std cons List: constant stack at 1,000,000 elements.
|
|
2
|
-
module cons_list
|
|
3
|
-
import std/Int
|
|
4
|
-
import std/Str
|
|
5
|
-
import std/Slice
|
|
6
|
-
import std/Array
|
|
7
|
-
import std/List
|
|
8
|
-
|
|
9
|
-
fun range(a: Int, b: Int) -> List[Int] =
|
|
10
|
-
if a >= b then Nil else Cons(head: a, tail: range(a + 1, b))
|
|
11
|
-
|
|
12
|
-
test "init_sum"
|
|
13
|
-
assert List(1, 2, 3, 4).fold(0, |a, x| a + x) == 10
|
|
14
|
-
assert List(1, 2, 3).length() == 3
|
|
15
|
-
assert { List(1, 2, 3).get(1) ?: -1 } == 2
|
|
16
|
-
assert List(1, 2, 3).get(5) == null
|
|
17
|
-
nil: List[Int] := Nil
|
|
18
|
-
assert nil.isEmpty()
|
|
19
|
-
assert !List(1).isEmpty()
|
|
20
|
-
|
|
21
|
-
test "map_filter_fold"
|
|
22
|
-
xs := List(1, 2, 3, 4, 5, 6)
|
|
23
|
-
ys := xs.map(|x| x * 2).filter(|x| x % 3 == 0)
|
|
24
|
-
assert ys.length() == 2
|
|
25
|
-
assert ys.fold(0, |a, x| a + x) == 18
|
|
26
|
-
|
|
27
|
-
test "reverse_append"
|
|
28
|
-
xs := List(1, 2, 3).reverse()
|
|
29
|
-
assert { xs.get(0) ?: -1 } == 3
|
|
30
|
-
assert { xs.get(2) ?: -1 } == 1
|
|
31
|
-
ys := List(1, 2).append(List(3, 4))
|
|
32
|
-
assert ys.equals(List(1, 2, 3, 4))
|
|
33
|
-
|
|
34
|
-
test "for_range"
|
|
35
|
-
total := 0
|
|
36
|
-
for x := range List(1, 2, 3, 4)
|
|
37
|
-
total = total + x
|
|
38
|
-
assert total == 10
|
|
39
|
-
big := 0
|
|
40
|
-
for x := range range(0, 1000000).items()
|
|
41
|
-
big = big + x
|
|
42
|
-
assert big == 499999500000
|
|
43
|
-
|
|
44
|
-
test "to_slice_round_trip"
|
|
45
|
-
s := List(1, 2, 3).map(|x| x * 2).toSlice()
|
|
46
|
-
assert s.equals(Slice(2, 4, 6))
|
|
47
|
-
assert List.fromSlice(s).equals(List(2, 4, 6))
|
|
48
|
-
|
|
49
|
-
test "to_array"
|
|
50
|
-
a := List(7, 8, 9).toArray()
|
|
51
|
-
assert a.length() == 3
|
|
52
|
-
assert a.get(2) == 9
|
|
53
|
-
|
|
54
|
-
test "map_million"
|
|
55
|
-
ys := range(0, 1000000).map(|x| x + 1)
|
|
56
|
-
assert ys.length() == 1000000
|
|
57
|
-
assert ys.fold(0, |a, x| a + x) == 500000500000
|
|
58
|
-
|
|
59
|
-
test "fold_million"
|
|
60
|
-
xs := range(0, 1000000)
|
|
61
|
-
assert xs.fold(0, |a, x| a + x) == 499999500000
|
|
62
|
-
assert xs.filter(|x| x % 2 == 0).length() == 500000
|
|
63
|
-
assert { xs.reverse().get(0) ?: -1 } == 999999
|
|
64
|
-
|
|
65
|
-
test "equals_and_toStr"
|
|
66
|
-
assert List(1, 2, 3).equals(List(1, 2, 3))
|
|
67
|
-
assert !List(1, 2, 3).equals(List(1, 2))
|
|
68
|
-
assert !List(1, 2).equals(List(1, 2, 3))
|
|
69
|
-
assert !List(1, 2, 3).equals(List(1, 2, 4))
|
|
70
|
-
assert List(1, 2, 3).toStr() == "[1,2,3]"
|
plum-examples/constants.plum
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
module constants
|
|
2
|
-
import std/Int
|
|
3
|
-
import std/Float
|
|
4
|
-
import std/Str
|
|
5
|
-
import std/Slice
|
|
6
|
-
|
|
7
|
-
# Constants and static functions live inside a type's body or a `primitive`
|
|
8
|
-
# namespace — there are no top-level constants. `Name = expr` declares an
|
|
9
|
-
# associated constant, a `fun` without `self` a static function. They are
|
|
10
|
-
# reached as `Type.Name` / `Type.f()` from anywhere, including inside the same
|
|
11
|
-
# body. Constant names are unique per type, not global.
|
|
12
|
-
|
|
13
|
-
# A `primitive` whose name is not a built-in type is a stateless namespace.
|
|
14
|
-
primitive Grid =
|
|
15
|
-
Width = 8
|
|
16
|
-
Height = Grid.Width * 2
|
|
17
|
-
CellScale = 2.5
|
|
18
|
-
Name = "plum"
|
|
19
|
-
DebugDraw = true
|
|
20
|
-
|
|
21
|
-
fun cells() -> Int =
|
|
22
|
-
Grid.Width * Grid.Height
|
|
23
|
-
|
|
24
|
-
record Tile(x: Int) =
|
|
25
|
-
|
|
26
|
-
Width = 100
|
|
27
|
-
|
|
28
|
-
fun offset(self) -> Int = Tile.Width + self.x
|
|
29
|
-
|
|
30
|
-
fun origin() -> Tile =
|
|
31
|
-
Tile(x: 0)
|
|
32
|
-
|
|
33
|
-
record Wrap[T](v: T) =
|
|
34
|
-
|
|
35
|
-
fun area(self) -> Int = Grid.Width * Grid.Height
|
|
36
|
-
|
|
37
|
-
struct Vec2(x: Float, y: Float) =
|
|
38
|
-
|
|
39
|
-
Zero = Vec2(x: 0.0, y: 0.0)
|
|
40
|
-
|
|
41
|
-
fun scaled(n: Float) -> Float = n * Grid.CellScale
|
|
42
|
-
|
|
43
|
-
primitive Shelf =
|
|
44
|
-
Sides = 4
|
|
45
|
-
Names = Slice("a", "b", "c")
|
|
46
|
-
|
|
47
|
-
fun sidesTwice() -> Int =
|
|
48
|
-
twice(Shelf.Sides)
|
|
49
|
-
|
|
50
|
-
fun twice(n: Int) -> Int =
|
|
51
|
-
n * 2
|
|
52
|
-
|
|
53
|
-
test "consts are usable in fns, methods, generics and derived consts"
|
|
54
|
-
assert Grid.Height == 16
|
|
55
|
-
assert Grid.cells() == 128
|
|
56
|
-
assert Tile(x: 1).offset() == 101
|
|
57
|
-
assert Wrap(v: 3.5).area() == 128
|
|
58
|
-
assert scaled(2.0) == 5.0
|
|
59
|
-
assert Grid.Name == "plum"
|
|
60
|
-
assert Grid.DebugDraw
|
|
61
|
-
|
|
62
|
-
test "the same const name can live on two types"
|
|
63
|
-
assert Grid.Width == 8
|
|
64
|
-
assert Tile.Width == 100
|
|
65
|
-
|
|
66
|
-
test "a struct can hold a constant instance of itself"
|
|
67
|
-
assert Vec2.Zero.x == 0.0
|
|
68
|
-
assert Tile.origin().x == 0
|
|
69
|
-
|
|
70
|
-
test "a primitive holds a list constant and a static fn reading a sibling const"
|
|
71
|
-
assert Shelf.Names.length() == 3
|
|
72
|
-
assert Shelf.sidesTwice() == 8
|
plum-examples/control_flow.plum
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
# Control flow:
|
|
1
|
+
# Control flow: branching and loops.
|
|
2
2
|
import std/Str
|
|
3
3
|
import std/Int
|
|
4
4
|
import std/Float
|
|
5
5
|
import std/Seq
|
|
6
|
+
import std/Slice
|
|
7
|
+
import std/Array
|
|
8
|
+
import std/Testing
|
|
9
|
+
|
|
10
|
+
# ==== control flow ====
|
|
11
|
+
|
|
12
|
+
# Control flow: `if`/`else` branching, `while` loops, and `for ... := range` iteration.
|
|
6
13
|
|
|
7
14
|
fun loopSum(limit: Int) -> Int =
|
|
8
15
|
total := 0
|
|
@@ -74,6 +81,25 @@ fun checkPositiveAndReturn(n: Int) -> Int =
|
|
|
74
81
|
assert n > 0
|
|
75
82
|
n
|
|
76
83
|
|
|
84
|
+
# ==== loops ====
|
|
85
|
+
|
|
86
|
+
# Loops: `while` condition loops, `for ... := range` iteration over ranges and
|
|
87
|
+
# collections, with inline filtering and mapping.
|
|
88
|
+
|
|
89
|
+
fun sumList(xs: Slice[Int]) -> Int =
|
|
90
|
+
total := 0
|
|
91
|
+
for x := range xs
|
|
92
|
+
total = total + x
|
|
93
|
+
return total
|
|
94
|
+
|
|
95
|
+
fun firstOver(xs: Slice[Int], limit: Int) -> Int =
|
|
96
|
+
for x := range xs
|
|
97
|
+
if x > limit
|
|
98
|
+
return x
|
|
99
|
+
return -1
|
|
100
|
+
|
|
101
|
+
# ==== control flow tests ====
|
|
102
|
+
|
|
77
103
|
test "tail match without return runs correctly"
|
|
78
104
|
assert bindExample(5) == 5
|
|
79
105
|
|
|
@@ -91,3 +117,109 @@ test "tail enum match without return runs correctly"
|
|
|
91
117
|
|
|
92
118
|
test "assert passes through on true"
|
|
93
119
|
assert checkPositiveAndReturn(5) == 5
|
|
120
|
+
|
|
121
|
+
# ==== loops tests ====
|
|
122
|
+
|
|
123
|
+
test "plain_iterable_for: List and Array"
|
|
124
|
+
xs := Slice(1, 2, 3, 4)
|
|
125
|
+
assert sumList(xs) == 10
|
|
126
|
+
arr := Array.filled(3, 7)
|
|
127
|
+
total := 0
|
|
128
|
+
for a := range arr
|
|
129
|
+
total = total + a
|
|
130
|
+
assert total == 21
|
|
131
|
+
|
|
132
|
+
test "index_value_binders: i, x"
|
|
133
|
+
xs := Slice(10, 20, 30)
|
|
134
|
+
acc := 0
|
|
135
|
+
for i, x := range xs
|
|
136
|
+
acc = acc + i * x
|
|
137
|
+
assert acc == 80
|
|
138
|
+
|
|
139
|
+
test "clause_if_keeps_when_true: if cond keeps the element, false skips it"
|
|
140
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
141
|
+
total := 0
|
|
142
|
+
for x := range xs, if x % 2 == 0
|
|
143
|
+
total = total + x
|
|
144
|
+
assert total == 12
|
|
145
|
+
|
|
146
|
+
test "clause_bind_scoped: name := expr is visible to later clauses and body"
|
|
147
|
+
xs := Slice(1, 2, 3, 4)
|
|
148
|
+
total := 0
|
|
149
|
+
for x := range xs, sq := x * x, if sq > 9
|
|
150
|
+
total = total + sq
|
|
151
|
+
assert total == 16
|
|
152
|
+
|
|
153
|
+
test "break_continue_return_semantics"
|
|
154
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
155
|
+
total := 0
|
|
156
|
+
for x := range xs
|
|
157
|
+
if x == 2
|
|
158
|
+
continue
|
|
159
|
+
if x == 5
|
|
160
|
+
break
|
|
161
|
+
total = total + x
|
|
162
|
+
assert total == 8
|
|
163
|
+
assert firstOver(xs, 3) == 4
|
|
164
|
+
assert firstOver(xs, 9) == -1
|
|
165
|
+
|
|
166
|
+
test "nested iterable loops"
|
|
167
|
+
xs := Slice(1, 2)
|
|
168
|
+
ys := Slice(10, 20)
|
|
169
|
+
total := 0
|
|
170
|
+
for x := range xs
|
|
171
|
+
for y := range ys
|
|
172
|
+
total = total + x * y
|
|
173
|
+
assert total == 90
|
|
174
|
+
|
|
175
|
+
test "range_int: 0 to n-1, empty for 0, continue still advances"
|
|
176
|
+
total := 0
|
|
177
|
+
for i := range 5
|
|
178
|
+
total = total + i
|
|
179
|
+
assert total == 10
|
|
180
|
+
n := 0
|
|
181
|
+
for _ := range 0
|
|
182
|
+
n = n + 1
|
|
183
|
+
assert n == 0
|
|
184
|
+
odd := 0
|
|
185
|
+
for i := range 6
|
|
186
|
+
if i % 2 == 0
|
|
187
|
+
continue
|
|
188
|
+
odd = odd + i
|
|
189
|
+
assert odd == 9
|
|
190
|
+
|
|
191
|
+
test "range_until_to_downTo: bounds incl. empty ranges"
|
|
192
|
+
a := 0
|
|
193
|
+
for i := range 2.until(6)
|
|
194
|
+
a = a + i
|
|
195
|
+
assert a == 14
|
|
196
|
+
b := 0
|
|
197
|
+
for i := range 1.to(4)
|
|
198
|
+
b = b + i
|
|
199
|
+
assert b == 10
|
|
200
|
+
c := 0
|
|
201
|
+
for i := range 4.downTo(1)
|
|
202
|
+
c = c * 10 + i
|
|
203
|
+
assert c == 4321
|
|
204
|
+
n := 0
|
|
205
|
+
for _ := range 5.until(5)
|
|
206
|
+
n = n + 1
|
|
207
|
+
for _ := range 1.downTo(2)
|
|
208
|
+
n = n + 1
|
|
209
|
+
for _ := range 3.to(2)
|
|
210
|
+
n = n + 1
|
|
211
|
+
assert n == 0
|
|
212
|
+
|
|
213
|
+
test "range_stepBy: stepBy on until and to"
|
|
214
|
+
total := 0
|
|
215
|
+
for i := range 0.until(10).stepBy(3)
|
|
216
|
+
total = total + i
|
|
217
|
+
assert total == 18
|
|
218
|
+
for i := range 1.to(10).stepBy(4)
|
|
219
|
+
total = total + i
|
|
220
|
+
assert total == 18 + 15
|
|
221
|
+
|
|
222
|
+
test "range_seq_terminal: a terminal over an Int range"
|
|
223
|
+
evens := for i := range 0.until(10).filter()
|
|
224
|
+
i % 2 == 0
|
|
225
|
+
assert evens.length() == 5
|
plum-examples/digit_names.plum
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
module digit_names
|
|
2
|
-
import std/Int
|
|
3
|
-
|
|
4
|
-
# Type and variant names may contain digits (`V2`, `A1`, `E1`). They are
|
|
5
|
-
# resolved by position: where a type or variant name is expected it is one;
|
|
6
|
-
# `Type.Name` consts such as `V2.K1` and `Int.Max` keep working.
|
|
7
|
-
|
|
8
|
-
enum V2 =
|
|
9
|
-
| A1
|
|
10
|
-
| B2
|
|
11
|
-
K1 = 40
|
|
12
|
-
|
|
13
|
-
record A1(n: Int)
|
|
14
|
-
|
|
15
|
-
error S1 = E1 | E2
|
|
16
|
-
|
|
17
|
-
fun fail(n: Int) -> S1!Int =
|
|
18
|
-
if n == 1
|
|
19
|
-
return error.E1
|
|
20
|
-
if n == 2
|
|
21
|
-
return error.E2
|
|
22
|
-
n
|
|
23
|
-
|
|
24
|
-
fun asInt(w: V2) -> Int =
|
|
25
|
-
match w
|
|
26
|
-
A1(n) => n
|
|
27
|
-
B2 => 0
|
|
28
|
-
|
|
29
|
-
fun payload(w: V2) -> Int =
|
|
30
|
-
if A1(n) := w
|
|
31
|
-
return n
|
|
32
|
-
0
|
|
33
|
-
|
|
34
|
-
fun isB2(w: V2) -> Int =
|
|
35
|
-
if B2 := w
|
|
36
|
-
return 1
|
|
37
|
-
0
|
|
38
|
-
|
|
39
|
-
test "digit_variant_match"
|
|
40
|
-
assert asInt(A1(n: 5)) == 5
|
|
41
|
-
x := B2
|
|
42
|
-
assert asInt(x) == 0
|
|
43
|
-
|
|
44
|
-
test "digit_variant_pattern_if"
|
|
45
|
-
assert payload(A1(n: 7)) == 7
|
|
46
|
-
assert payload(B2) == 0
|
|
47
|
-
w := B2
|
|
48
|
-
assert isB2(w) == 1
|
|
49
|
-
assert isB2(A1(n: 1)) == 0
|
|
50
|
-
|
|
51
|
-
test "digit_error_names"
|
|
52
|
-
a := fail(3) catch 0
|
|
53
|
-
assert a == 3
|
|
54
|
-
b := fail(1) catch 9
|
|
55
|
-
assert b == 9
|
|
56
|
-
|
|
57
|
-
test "digit_enum_const_still_works"
|
|
58
|
-
assert V2.K1 == 40
|
|
59
|
-
assert Int.Max > 0
|
plum-examples/{error_propagation.plum → errors.plum}
RENAMED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
# Errors and optionals: error propagation and T?.
|
|
1
2
|
import std/Int
|
|
2
3
|
import std/Float
|
|
3
4
|
import std/Str
|
|
4
5
|
import std/Slice
|
|
5
6
|
import std/Map
|
|
6
7
|
|
|
8
|
+
# ==== error propagation ====
|
|
9
|
+
|
|
7
10
|
# `!T` is "a `T` or an error". An error is a value written `error.Name` (for
|
|
8
11
|
# example `return error.NegativeNum`): it needs no declaration and carries no
|
|
9
12
|
# data. A function's error set is inferred from its body (every error value it
|
|
@@ -158,6 +161,103 @@ record Ages[T](items: Slice[T]) =
|
|
|
158
161
|
|
|
159
162
|
fun parseAges(xs: Slice[Str]) -> !Ages[Age] = Ages.parseAll(xs)
|
|
160
163
|
|
|
164
|
+
# ==== optionals ====
|
|
165
|
+
|
|
166
|
+
# Optionals: `T?` is a `T` or `null`. A plain `T` in a `T?` slot is the present
|
|
167
|
+
# value. An optional is handled with `?:` (Kotlin elvis), `if x := opt`,
|
|
168
|
+
# `while x := opt`, `opt!` (crashes on null) and `opt == null`; `catch` and `try`
|
|
169
|
+
# are for errors only. `!T?` is an error union whose success value is an optional.
|
|
170
|
+
#
|
|
171
|
+
# `obj?.field` / `obj?.method(args)` is safe navigation: it reaches into an
|
|
172
|
+
# optional without unwrapping it and passes null through. A field that is itself
|
|
173
|
+
# a `T?` flattens, so `u?.target?.name` is one `Str?`. It is sugar for
|
|
174
|
+
# `obj.andThen(|v| v.field)`.
|
|
175
|
+
|
|
176
|
+
record Box(label: Str)
|
|
177
|
+
|
|
178
|
+
record Holder(mut item: Box?, mut count: Int?)
|
|
179
|
+
|
|
180
|
+
fun maybeBox(c: Bool) -> Box? =
|
|
181
|
+
if c
|
|
182
|
+
return Box(label: "in")
|
|
183
|
+
return null
|
|
184
|
+
|
|
185
|
+
fun maybeInt(c: Bool) -> Int? =
|
|
186
|
+
if c
|
|
187
|
+
return 7
|
|
188
|
+
null
|
|
189
|
+
|
|
190
|
+
fun orFive(n: Int? = null) -> Int =
|
|
191
|
+
n ?: 5
|
|
192
|
+
|
|
193
|
+
fun failOnNull(c: Bool) -> error{Absent}!Int =
|
|
194
|
+
n := maybeInt(c) ?: return error.Absent
|
|
195
|
+
n + 1
|
|
196
|
+
|
|
197
|
+
fun nullOnNull(c: Bool) -> Int? =
|
|
198
|
+
n := maybeInt(c) ?: return null
|
|
199
|
+
n * 2
|
|
200
|
+
|
|
201
|
+
fun chainDefault(a: Int?, b: Int?) -> Int =
|
|
202
|
+
a ?: b ?: 99
|
|
203
|
+
|
|
204
|
+
fun fallible(n: Int) -> error{Bad}!Int? =
|
|
205
|
+
if n < 0
|
|
206
|
+
return error.Bad
|
|
207
|
+
if n == 0
|
|
208
|
+
return null
|
|
209
|
+
n
|
|
210
|
+
|
|
211
|
+
fun viaTry(n: Int) -> error{Bad}!Int =
|
|
212
|
+
v := try fallible(n)
|
|
213
|
+
v ?: -1
|
|
214
|
+
|
|
215
|
+
fun nextItem(items: Slice[Int], i: Int) -> Int? =
|
|
216
|
+
if i < items.length()
|
|
217
|
+
return items.get(i)!
|
|
218
|
+
null
|
|
219
|
+
|
|
220
|
+
fun sparse() -> Slice[Int?] =
|
|
221
|
+
Slice(1, null, 3)
|
|
222
|
+
|
|
223
|
+
fun sumBig() -> Int =
|
|
224
|
+
Int.Max + Int.Min
|
|
225
|
+
|
|
226
|
+
record Person(name: Str, age: Int) =
|
|
227
|
+
|
|
228
|
+
fun birthday(self) -> Int =
|
|
229
|
+
self.age + 1
|
|
230
|
+
|
|
231
|
+
record Target(name: Str)
|
|
232
|
+
|
|
233
|
+
record User(name: Str, target: Target?)
|
|
234
|
+
|
|
235
|
+
fun personName(p: Person?) -> Str? =
|
|
236
|
+
p?.name
|
|
237
|
+
|
|
238
|
+
fun personBirthday(p: Person?) -> Int? =
|
|
239
|
+
p?.birthday()
|
|
240
|
+
|
|
241
|
+
fun somePerson() -> Person? =
|
|
242
|
+
Person(name: "Ann", age: 3)
|
|
243
|
+
|
|
244
|
+
fun noPerson() -> Person? =
|
|
245
|
+
return null
|
|
246
|
+
|
|
247
|
+
fun noTarget() -> Target? =
|
|
248
|
+
return null
|
|
249
|
+
|
|
250
|
+
fun maybeCount(n: Int? = null) -> Int? =
|
|
251
|
+
n
|
|
252
|
+
|
|
253
|
+
fun noUser() -> User? =
|
|
254
|
+
return null
|
|
255
|
+
|
|
256
|
+
fun targetName(u: User?) -> Str? =
|
|
257
|
+
u?.target?.name
|
|
258
|
+
|
|
259
|
+
# ==== error propagation tests ====
|
|
260
|
+
|
|
161
261
|
test "try operator unwraps the value and propagates it through two calls"
|
|
162
262
|
assert sumTwo("2", "3")! == 5
|
|
163
263
|
|
|
@@ -253,3 +353,141 @@ test "a Map can hold error unions as values"
|
|
|
253
353
|
assert lookup(m) == 7
|
|
254
354
|
m.set(1, negOrSeven(-2))
|
|
255
355
|
assert lookup(m) == 0
|
|
356
|
+
|
|
357
|
+
# ==== optionals tests ====
|
|
358
|
+
|
|
359
|
+
test "null_return_absent"
|
|
360
|
+
assert maybeBox(false) == null
|
|
361
|
+
assert maybeBox(true) != null
|
|
362
|
+
assert maybeInt(false) == null
|
|
363
|
+
assert null != maybeInt(true)
|
|
364
|
+
|
|
365
|
+
test "null_field_and_arg"
|
|
366
|
+
h := Holder(item: null, count: null)
|
|
367
|
+
assert h.item == null
|
|
368
|
+
assert h.count == null
|
|
369
|
+
h2 := Holder(item: Box(label: "x"), count: 3)
|
|
370
|
+
assert h2.count! == 3
|
|
371
|
+
assert orFive(null) == 5
|
|
372
|
+
assert orFive(8) == 8
|
|
373
|
+
xs := sparse()
|
|
374
|
+
assert xs.length() == 3
|
|
375
|
+
|
|
376
|
+
test "elvis_default"
|
|
377
|
+
assert {maybeInt(false) ?: 4} == 4
|
|
378
|
+
assert {maybeInt(true) ?: 4} == 7
|
|
379
|
+
assert {maybeBox(false)?.label ?: "none"} == "none"
|
|
380
|
+
assert {maybeBox(true)?.label ?: "none"} == "in"
|
|
381
|
+
|
|
382
|
+
test "elvis_return_error"
|
|
383
|
+
assert {failOnNull(true) catch 0} == 8
|
|
384
|
+
assert {failOnNull(false) catch 0} == 0
|
|
385
|
+
|
|
386
|
+
test "elvis_return_null"
|
|
387
|
+
assert nullOnNull(true)! == 14
|
|
388
|
+
assert nullOnNull(false) == null
|
|
389
|
+
|
|
390
|
+
test "elvis_chain_right_assoc"
|
|
391
|
+
assert chainDefault(null, null) == 99
|
|
392
|
+
assert chainDefault(null, 2) == 2
|
|
393
|
+
assert chainDefault(1, 2) == 1
|
|
394
|
+
|
|
395
|
+
test "if_bind_optional"
|
|
396
|
+
r := 0
|
|
397
|
+
if x := maybeInt(true)
|
|
398
|
+
r = x
|
|
399
|
+
else
|
|
400
|
+
r = -1
|
|
401
|
+
assert r == 7
|
|
402
|
+
if y := maybeInt(false)
|
|
403
|
+
r = y
|
|
404
|
+
else
|
|
405
|
+
r = -1
|
|
406
|
+
assert r == -1
|
|
407
|
+
assert {if v := maybeInt(true) then v + 1 else 0} == 8
|
|
408
|
+
assert {if w := maybeInt(false) then w + 1 else 0} == 0
|
|
409
|
+
if b := maybeBox(true)
|
|
410
|
+
assert b.label == "in"
|
|
411
|
+
|
|
412
|
+
test "while_bind_optional"
|
|
413
|
+
items := Slice(3, 2, 1)
|
|
414
|
+
total := 0
|
|
415
|
+
i := 0
|
|
416
|
+
while v := nextItem(items, i)
|
|
417
|
+
total = total + v
|
|
418
|
+
i = i + 1
|
|
419
|
+
assert total == 6
|
|
420
|
+
|
|
421
|
+
test "safe_nav_optional_flattens"
|
|
422
|
+
assert maybeBox(true)?.label! == "in"
|
|
423
|
+
assert maybeBox(false)?.label == null
|
|
424
|
+
|
|
425
|
+
test "null_eq_ref and null_eq_int"
|
|
426
|
+
b := maybeBox(false)
|
|
427
|
+
assert b == null
|
|
428
|
+
n := maybeInt(true)
|
|
429
|
+
assert n != null
|
|
430
|
+
assert !{n == null}
|
|
431
|
+
|
|
432
|
+
test "error_union_of_optional"
|
|
433
|
+
assert {viaTry(5) catch 0} == 5
|
|
434
|
+
assert {viaTry(0) catch 0} == -1
|
|
435
|
+
assert {viaTry(-3) catch 100} == 100
|
|
436
|
+
x := fallible(0)!
|
|
437
|
+
assert x == null
|
|
438
|
+
y := fallible(4)!
|
|
439
|
+
assert y! == 4
|
|
440
|
+
|
|
441
|
+
test "int_min_literal"
|
|
442
|
+
assert Int.Min == -0x8000_0000_0000_0000
|
|
443
|
+
assert sumBig() == -1
|
|
444
|
+
|
|
445
|
+
test "reassigning an optional local or field to null or a value"
|
|
446
|
+
o := maybeInt(true)
|
|
447
|
+
o = null
|
|
448
|
+
assert o == null
|
|
449
|
+
o = 4
|
|
450
|
+
assert o! == 4
|
|
451
|
+
b := maybeBox(true)
|
|
452
|
+
b = null
|
|
453
|
+
assert b == null
|
|
454
|
+
h := Holder(item: Box(label: "x"), count: 2)
|
|
455
|
+
h.count = null
|
|
456
|
+
h.item = null
|
|
457
|
+
assert h.count == null
|
|
458
|
+
assert h.item == null
|
|
459
|
+
h.count = 5
|
|
460
|
+
h.item = Box(label: "y")
|
|
461
|
+
assert h.count! == 5
|
|
462
|
+
assert h.item!.label == "y"
|
|
463
|
+
|
|
464
|
+
test "safe navigation maps a field access over a present value, passes absence through"
|
|
465
|
+
assert personName(somePerson())! == "Ann"
|
|
466
|
+
assert {personName(noPerson()) ?: "gone"} == "gone"
|
|
467
|
+
|
|
468
|
+
test "safe navigation maps a method call over a present value"
|
|
469
|
+
assert personBirthday(somePerson())! == 4
|
|
470
|
+
assert {personBirthday(noPerson()) ?: -1} == -1
|
|
471
|
+
|
|
472
|
+
test "safe navigation chains through a nullable field"
|
|
473
|
+
full := User(name: "u", target: Target(name: "home"))
|
|
474
|
+
assert {targetName(full) ?: "none"} == "home"
|
|
475
|
+
assert {targetName(User(name: "v", target: noTarget())) ?: "none"} == "none"
|
|
476
|
+
assert {targetName(noUser()) ?: "none"} == "none"
|
|
477
|
+
|
|
478
|
+
test "?: handles a nullable with a diverging or computed default"
|
|
479
|
+
p := somePerson() ?: crash "missing"
|
|
480
|
+
assert p.age == 3
|
|
481
|
+
q := noPerson() ?: Person(name: "gone", age: 1)
|
|
482
|
+
assert q.name == "gone"
|
|
483
|
+
|
|
484
|
+
test "null is the absent value in an argument, a list element and a default"
|
|
485
|
+
u := User(name: "w", target: null)
|
|
486
|
+
assert {targetName(u) ?: "none"} == "none"
|
|
487
|
+
xs := sparse()
|
|
488
|
+
second := xs.get(1)!
|
|
489
|
+
assert {second ?: 9} == 9
|
|
490
|
+
first := xs.get(0)!
|
|
491
|
+
assert first! == 1
|
|
492
|
+
assert {maybeCount() ?: 5} == 5
|
|
493
|
+
assert maybeCount(7)! == 7
|
plum-examples/forms.plum
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
# User-defined `fun*` forms with `yield`: sources, folds and methods written in
|
|
2
|
-
# plum and expanded at the call site (the built-in forms are in seq.plum).
|
|
3
|
-
import std/Str
|
|
4
|
-
import std/Int
|
|
5
|
-
import std/Float
|
|
6
|
-
import std/Slice
|
|
7
|
-
import std/Testing
|
|
8
|
-
import std/Seq
|
|
9
|
-
|
|
10
|
-
# for-forms: `for binders := range form(args), clauses` + body. A form is a `fun*`
|
|
11
|
-
# with a yield type (`*T`); each `yield` runs the `for` body, and the whole
|
|
12
|
-
# thing is expanded at the call site, so there are no closures. See
|
|
13
|
-
# plum-std/Seq.plum and plum-core/src/forms.rs.
|
|
14
|
-
|
|
15
|
-
record Fighter(name: Str, hp: Int, dead: Bool)
|
|
16
|
-
|
|
17
|
-
fun sampleFighters() -> Slice[Fighter] =
|
|
18
|
-
Slice(Fighter(name: "a", hp: 10, dead: false), Fighter(name: "b", hp: 0, dead: true), Fighter(name: "c", hp: 7, dead: false))
|
|
19
|
-
|
|
20
|
-
# a user form: the sum of the body's value over a list
|
|
21
|
-
fun* sumBy(xs: Slice[T]) *T -> Int =
|
|
22
|
-
total := 0
|
|
23
|
-
for x := range xs
|
|
24
|
-
v := yield x
|
|
25
|
-
total = total + v
|
|
26
|
-
total
|
|
27
|
-
|
|
28
|
-
# a user form with two binders and a statement body: the 8 neighbours of (x, y)
|
|
29
|
-
fun* neighbours(x: Int, y: Int) *(Int, Int) =
|
|
30
|
-
for dx := range 3
|
|
31
|
-
for dy := range 3
|
|
32
|
-
if dx != 1 || dy != 1
|
|
33
|
-
yield x + dx - 1, y + dy - 1
|
|
34
|
-
|
|
35
|
-
# a user form built from another form: `yield` sits inside `each`'s body
|
|
36
|
-
fun* sumEvens(xs: Slice[Int]) *Int -> Int =
|
|
37
|
-
total := 0
|
|
38
|
-
for x := range xs.each(), if x % 2 == 0
|
|
39
|
-
v := yield x
|
|
40
|
-
total = total + v
|
|
41
|
-
total
|
|
42
|
-
|
|
43
|
-
fun firstBig(xs: Slice[Int]) -> Int =
|
|
44
|
-
for x := range xs.each()
|
|
45
|
-
if x > 10
|
|
46
|
-
return x
|
|
47
|
-
return -1
|
|
48
|
-
|
|
49
|
-
fun returnedForm(xs: Slice[Int]) -> Slice[Int] =
|
|
50
|
-
return for x := range xs.map()
|
|
51
|
-
x + 1
|
|
52
|
-
|
|
53
|
-
fun tailForm(xs: Slice[Int]) -> Int =
|
|
54
|
-
for x := range sumBy(xs)
|
|
55
|
-
x * 2
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# A type with a fun* method: `self` is bound to the receiver at the use site.
|
|
59
|
-
record Countdown(from: Int) =
|
|
60
|
-
|
|
61
|
-
fun* ticks(self) *Int =
|
|
62
|
-
i := self.from
|
|
63
|
-
while i > 0
|
|
64
|
-
cur := i
|
|
65
|
-
i = i - 1
|
|
66
|
-
yield cur
|
|
67
|
-
|
|
68
|
-
# `for a, b := range countdown`: two binders pick `entries`.
|
|
69
|
-
fun* entries(self) *(Int, Int) =
|
|
70
|
-
i := self.from
|
|
71
|
-
while i > 0
|
|
72
|
-
cur := i
|
|
73
|
-
i = i - 1
|
|
74
|
-
yield cur, cur * cur
|
|
75
|
-
|
|
76
|
-
test "user_form_sumBy"
|
|
77
|
-
fighters := sampleFighters()
|
|
78
|
-
hp := for u := range sumBy(fighters)
|
|
79
|
-
u.hp
|
|
80
|
-
assert hp == 17
|
|
81
|
-
odd := for x := range sumBy(Slice(1, 2, 3, 4, 5)), if x % 2 == 1
|
|
82
|
-
x
|
|
83
|
-
assert odd == 9
|
|
84
|
-
|
|
85
|
-
test "user_form_neighbours_grid: two binders, statement body"
|
|
86
|
-
count := 0
|
|
87
|
-
sum := 0
|
|
88
|
-
for nx, ny := range neighbours(5, 5), if nx > 5
|
|
89
|
-
count = count + 1
|
|
90
|
-
sum = sum + ny
|
|
91
|
-
assert count == 3
|
|
92
|
-
assert sum == 15
|
|
93
|
-
|
|
94
|
-
test "break_continue_return_semantics"
|
|
95
|
-
xs := Slice(1, 2, 3, 4, 5)
|
|
96
|
-
total := 0
|
|
97
|
-
for x := range xs.each()
|
|
98
|
-
if x == 2
|
|
99
|
-
continue
|
|
100
|
-
if x == 5
|
|
101
|
-
break
|
|
102
|
-
total = total + x
|
|
103
|
-
assert total == 8
|
|
104
|
-
assert firstBig(Slice(1, 20, 30)) == 20
|
|
105
|
-
assert firstBig(xs) == -1
|
|
106
|
-
prefix := for x := range xs.filter()
|
|
107
|
-
if x == 4
|
|
108
|
-
break
|
|
109
|
-
x > 1
|
|
110
|
-
assert prefix.length() == 2
|
|
111
|
-
|
|
112
|
-
test "composed form: a form that uses another form"
|
|
113
|
-
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
114
|
-
total := for x := range sumEvens(xs)
|
|
115
|
-
x * 10
|
|
116
|
-
assert total == 120
|
|
117
|
-
|
|
118
|
-
test "forms nest: a form inside a form body"
|
|
119
|
-
xs := Slice(1, 2)
|
|
120
|
-
ys := Slice(10, 20)
|
|
121
|
-
total := for a := range sumBy(xs)
|
|
122
|
-
inner := for b := range sumBy(ys)
|
|
123
|
-
a * b
|
|
124
|
-
inner
|
|
125
|
-
assert total == 90
|
|
126
|
-
|
|
127
|
-
test "return for-form: the form's value is returned"
|
|
128
|
-
ys := returnedForm(Slice(1, 2, 3))
|
|
129
|
-
assert ys.length() == 3
|
|
130
|
-
assert ys.at(2) == 4
|
|
131
|
-
|
|
132
|
-
test "tail for-form: the last statement of a fn is its value"
|
|
133
|
-
assert tailForm(Slice(1, 2, 3)) == 12
|
|
134
|
-
|
|
135
|
-
test "fun_star_method_uses_self"
|
|
136
|
-
c := Countdown(from: 4)
|
|
137
|
-
seen := 0
|
|
138
|
-
for t := range c.ticks()
|
|
139
|
-
seen = seen * 10 + t
|
|
140
|
-
assert seen == 4321
|
|
141
|
-
evens := for t := range c.ticks().map(), if t % 2 == 0
|
|
142
|
-
t * 100
|
|
143
|
-
assert evens.length() == 2
|
|
144
|
-
assert evens.at(0) == 400
|
|
145
|
-
|
|
146
|
-
test "two_binder_uses_entries_method: a custom type's entries()"
|
|
147
|
-
c := Countdown(from: 3)
|
|
148
|
-
total := 0
|
|
149
|
-
for n, sq := range c
|
|
150
|
-
total = total + n * 1000 + sq
|
|
151
|
-
assert total == 3 * 1000 + 9 + 2 * 1000 + 4 + 1000 + 1
|
|
152
|
-
for _, sq := range c
|
|
153
|
-
total = total + sq
|
|
154
|
-
assert total == 6014 + 14
|
|
155
|
-
|
plum-examples/functions.plum
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# Functions: top-level function declarations, typed arguments, default and
|
|
2
|
-
# variadic parameters, return expressions, and recursion.
|
|
3
|
-
import std/Str
|
|
4
|
-
import std/Int
|
|
5
|
-
import std/Float
|
|
6
|
-
|
|
7
|
-
fun addInts(a: Int, b: Int) -> Int =
|
|
8
|
-
a + b
|
|
9
|
-
|
|
10
|
-
fun greet() =
|
|
11
|
-
todo
|
|
12
|
-
|
|
13
|
-
fun withDefault(a: Int, step: Int = 1) -> Int =
|
|
14
|
-
a + step
|
|
15
|
-
|
|
16
|
-
fun sumAll(nums: ...Int) -> Int =
|
|
17
|
-
total := 0
|
|
18
|
-
for v := range nums
|
|
19
|
-
total = total + v
|
|
20
|
-
total
|
|
21
|
-
|
|
22
|
-
fun wrap(value: T) -> Bool =
|
|
23
|
-
true
|
|
24
|
-
|
|
25
|
-
fun pair(first: T, second: U) -> Bool =
|
|
26
|
-
true
|
|
27
|
-
|
|
28
|
-
fun useWrap() -> Bool =
|
|
29
|
-
wrap(5)
|
|
30
|
-
|
|
31
|
-
fun usePair() -> Bool =
|
|
32
|
-
pair(1, "x")
|
|
33
|
-
|
|
34
|
-
# ---- recursion, defaults and variadics ----
|
|
35
|
-
|
|
36
|
-
fun factorial(x: Int) -> Int =
|
|
37
|
-
if x < 2
|
|
38
|
-
return 1
|
|
39
|
-
return x * factorial(x - 1)
|
|
40
|
-
|
|
41
|
-
# Regression test: `double(...)` (an all-lowercase, no-uppercase, no-underscore
|
|
42
|
-
# callee) used to fail to parse at all — `var_identifier` and `fn_identifier`
|
|
43
|
-
# both matched its text and the grammar's lexer would nondeterministically
|
|
44
|
-
# commit to `var_identifier`, breaking every such call site.
|
|
45
|
-
fun double(n: Int) -> Int =
|
|
46
|
-
n * 2
|
|
47
|
-
|
|
48
|
-
fun combineVariadic(prefix: Int, rest: ...Int) -> Int =
|
|
49
|
-
prefix
|
|
50
|
-
|
|
51
|
-
fun weighted(nums: ...Int) -> Int =
|
|
52
|
-
total := 0
|
|
53
|
-
for i, v := range nums
|
|
54
|
-
total = total + i * v
|
|
55
|
-
total
|
|
56
|
-
|
|
57
|
-
test "factorial runs correctly"
|
|
58
|
-
assert factorial(5) == 120
|
|
59
|
-
|
|
60
|
-
test "lowercase single word function call runs correctly"
|
|
61
|
-
assert double(21) == 42
|
|
62
|
-
|
|
63
|
-
test "variadic call with varying trailing arg counts runs correctly"
|
|
64
|
-
a := combineVariadic(10)
|
|
65
|
-
b := combineVariadic(20, 1)
|
|
66
|
-
c := combineVariadic(30, 1, 2, 3)
|
|
67
|
-
assert a + b + c == 60
|
|
68
|
-
|
|
69
|
-
test "sum all variadic int runs correctly"
|
|
70
|
-
assert sumAll(1, 2, 3, 4) == 10
|
|
71
|
-
|
|
72
|
-
test "sum all variadic int with zero args runs correctly"
|
|
73
|
-
assert sumAll() == 0
|
|
74
|
-
|
|
75
|
-
test "for over a variadic binds index and element"
|
|
76
|
-
assert weighted() == 0
|
|
77
|
-
assert weighted(5, 10, 20) == 50
|
plum-examples/{assets → games/assets}/gameover.wav
RENAMED
|
File without changes
|
plum-examples/{assets → games/assets}/hit.wav
RENAMED
|
File without changes
|
plum-examples/{assets → games/assets}/music.wav
RENAMED
|
File without changes
|
plum-examples/{assets → games/assets}/pickup.wav
RENAMED
|
File without changes
|
plum-examples/{assets → games/assets}/sprites.png
RENAMED
|
File without changes
|
plum-examples/games/functional/game_state_view.plum
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
# Orb Run as
|
|
1
|
+
# Orb Run as components (`plum game plum-examples/games/functional/game_state_view.plum`): the same
|
|
2
2
|
# game as game_pure_scene.plum (same arena, rules, sprites, sounds, keys and scoring),
|
|
3
|
-
# written the
|
|
3
|
+
# written in the component style of Elm and React but purely functional. Steer the ship
|
|
4
|
-
# WASD, collect the drifting orbs and avoid the mines. Three
|
|
4
|
+
# with the arrow keys or WASD, collect the drifting orbs and avoid the mines. Three
|
|
5
|
-
# "Again" button restarts after the game is over.
|
|
5
|
+
# lives; Enter, R or the "Again" button restarts after the game is over.
|
|
6
6
|
#
|
|
7
|
+
# Each kind (`Ship`, `Orb`, `Mine`, `Star`) keeps its data, its behaviour and its
|
|
8
|
+
# drawing together: `update(self, ..)` returns the next value of the kind (a spread,
|
|
9
|
+
# nothing is changed in place) and `render(self, ..)` returns its `SceneNode` picture.
|
|
7
|
-
#
|
|
10
|
+
# `State` is the same for the whole game: the components, the score, a `Random` and the
|
|
8
|
-
# mines (cons `List` lists keyed by id), the score, the random seed and the screen
|
|
9
|
-
# size. `Msg` is what can happen to it (a tick of time, a resize, a restart
|
|
11
|
+
# screen size. `Msg` is what can happen to it (a tick of time, a resize, a restart, a
|
|
10
|
-
#
|
|
12
|
+
# Ui event); `State.update(msg)` is a short sequence of named steps and
|
|
11
|
-
# `
|
|
13
|
+
# `State.render()` builds a fresh picture every frame, which `Picture.draw` interprets
|
|
12
|
-
# `Picture.draw` interprets. Both are structural recursion over `Link` and `List[SceneNode]`:
|
|
13
|
-
#
|
|
14
|
+
# (the compiler fuses the two, so the tree is never built). There is no retained
|
|
14
|
-
#
|
|
15
|
+
# scene: the restart button is a `Ui.button` in the picture; the host turns the mouse
|
|
15
|
-
#
|
|
16
|
+
# into a `UiInput`, finds what is under it with `Picture.hitId` on the picture the
|
|
16
|
-
#
|
|
17
|
+
# player is looking at, runs `Ui.step`, and feeds each `UiEvent` to `State.update`.
|
|
17
|
-
# The game-over title and the ship's hurt flash are `Anim`s of time since they began.
|
|
18
|
+
# The game-over title and the ship's hurt flash are `Anim`s of time since they began.
|
|
19
|
+
# The `World` record is the host shell: it holds the current `State`, reads the keyboard
|
|
18
|
-
#
|
|
20
|
+
# and mouse into messages, and plays sounds.
|
|
19
21
|
module game_state_view
|
|
20
22
|
import std/Str
|
|
21
23
|
import std/Int
|
|
@@ -23,12 +25,14 @@ import std/Float
|
|
|
23
25
|
import std/Slice
|
|
24
26
|
import std/List
|
|
25
27
|
import std/Math
|
|
28
|
+
import std/Random
|
|
26
29
|
import std/game/Vec2
|
|
27
30
|
import std/game/Color
|
|
28
31
|
import std/game/Game
|
|
29
32
|
import std/game/functional/Picture
|
|
30
33
|
import std/game/functional/Anim
|
|
31
34
|
import std/game/functional/Ui
|
|
35
|
+
import std/Io
|
|
32
36
|
|
|
33
37
|
primitive Rules =
|
|
34
38
|
ArenaW = 800.0
|
|
@@ -46,16 +50,6 @@ primitive Rules =
|
|
|
46
50
|
Size = 48.0
|
|
47
51
|
Half = 24.0
|
|
48
52
|
|
|
49
|
-
# Where each layer sits among the root's children in the picture `view` builds.
|
|
50
|
-
primitive Slot =
|
|
51
|
-
Backdrop = 0
|
|
52
|
-
Stars = 1
|
|
53
|
-
Mines = 2
|
|
54
|
-
Orbs = 3
|
|
55
|
-
Ship = 4
|
|
56
|
-
Hud = 5
|
|
57
|
-
Overlay = 6
|
|
58
|
-
|
|
59
53
|
# Entity ids: orb `i` is `Id.Orb + i`, mine `i` is `Id.Mine + i`.
|
|
60
54
|
primitive Id =
|
|
61
55
|
Orb = 100
|
|
@@ -65,71 +59,8 @@ primitive Id =
|
|
|
65
59
|
# A position and velocity: what a spawn returns and what drifters are bounced with.
|
|
66
60
|
struct Motion(x: Float, y: Float, vx: Float, vy: Float)
|
|
67
61
|
|
|
68
|
-
# A random spot
|
|
62
|
+
# A random spot and the generator to continue from.
|
|
69
|
-
struct Spawn(x: Float, y: Float, vx: Float, vy: Float, seed: Int)
|
|
70
|
-
|
|
71
|
-
struct ShipState(x: Float, y: Float, vx: Float, vy: Float, rotation: Float, hurt: Float)
|
|
72
|
-
|
|
73
|
-
struct Orb(id: Int, x: Float, y: Float, vx: Float, vy: Float, phase: Float, clock: Float)
|
|
74
|
-
|
|
75
|
-
struct Mine(id: Int, x: Float, y: Float, vx: Float, vy: Float, rotation: Float, spin: Float, clock: Float)
|
|
76
|
-
|
|
77
|
-
# Everything that changes during a run. `sheet` and `target` are resource handles
|
|
78
|
-
# the view needs: the sprite sheet and the render target of the game-over filter.
|
|
79
|
-
record State(
|
|
80
|
-
ship: ShipState,
|
|
81
|
-
orbs: List[Orb],
|
|
82
|
-
mines: List[Mine],
|
|
83
|
-
seed: Int,
|
|
84
|
-
score: Int,
|
|
85
|
-
best: Int,
|
|
86
|
-
lives: Int,
|
|
87
|
-
over: Bool,
|
|
88
|
-
clock: Float,
|
|
89
|
-
overClock: Float,
|
|
90
|
-
hurtAt: Float,
|
|
91
|
-
overAt: Float,
|
|
92
|
-
ui: UiState,
|
|
93
|
-
sw: Float,
|
|
94
|
-
sh: Float,
|
|
95
|
-
sheet: Int,
|
|
96
|
-
target: Int)
|
|
97
|
-
|
|
98
|
-
# What can happen to the state.
|
|
99
|
-
enum Msg =
|
|
100
|
-
|
|
63
|
+
record Spawned(at: Motion, rng: Random)
|
|
101
|
-
| Resize(w: Float, h: Float)
|
|
102
|
-
| Restart(seed: Int)
|
|
103
|
-
| UiEv(event: UiEvent)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
fun mapLink(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
107
|
-
Nil => Nil
|
|
108
|
-
Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
|
|
109
|
-
|
|
110
|
-
fun lenLink(xs: List[T]) -> Int = match xs
|
|
111
|
-
Nil => 0
|
|
112
|
-
Cons(_, t) => 1 + lenLink(t)
|
|
113
|
-
|
|
114
|
-
# `xs` with `x` added at the end.
|
|
115
|
-
fun snoc(xs: List[T], x: T) -> List[T] = match xs
|
|
116
|
-
Nil => Cons(head: x, tail: Nil)
|
|
117
|
-
Cons(h, t) => Cons(head: h, tail: snoc(t, x))
|
|
118
|
-
|
|
119
|
-
# `xs` with the element at `i` replaced by `x`.
|
|
120
|
-
fun replaceNth(xs: List[T], i: Int, x: T) -> List[T] = match xs
|
|
121
|
-
Nil => Nil
|
|
122
|
-
Cons(h, t) => if i <= 0 then Cons(head: x, tail: t) else Cons(head: h, tail: replaceNth(t, i - 1, x))
|
|
123
|
-
|
|
124
|
-
# The element at `i`, or `dflt` past the end.
|
|
125
|
-
fun nthOr(xs: List[T], i: Int, dflt: T) -> T = match xs
|
|
126
|
-
Nil => dflt
|
|
127
|
-
Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
|
|
128
|
-
|
|
129
|
-
# The nodes f(x) for every x of `xs`, followed by `rest`.
|
|
130
|
-
fun toNodes(xs: List[T], f: fn(T) -> SceneNode, rest: List[SceneNode]) -> List[SceneNode] = match xs
|
|
131
|
-
Nil => rest
|
|
132
|
-
Cons(h, t) => Cons(head: f(h), tail: toNodes(t, f, rest))
|
|
133
64
|
|
|
134
65
|
# --- small pure helpers ---
|
|
135
66
|
|
|
@@ -137,269 +68,51 @@ fun toNodes(xs: List[T], f: fn(T) -> SceneNode, rest: List[SceneNode]) -> List[S
|
|
|
137
68
|
fun frameOf(clock: Float, count: Int, fps: Float) -> Int =
|
|
138
69
|
Int(clock * fps) % count
|
|
139
70
|
|
|
140
|
-
fun
|
|
71
|
+
fun nothing() -> SceneNode =
|
|
141
|
-
|
|
72
|
+
Group(transform: Identity, children: Nil)
|
|
73
|
+
|
|
74
|
+
fun cell(sheet: Int, x: Float, y: Float, size: Float, col: Int, row: Int) -> SceneNode =
|
|
75
|
+
Sprite(
|
|
76
|
+
tex: sheet, x: x, y: y, w: size, h: size, srcX: Float(col) * Rules.Cell, srcY: Float(row) * Rules.Cell,
|
|
77
|
+
srcW: Rules.Cell, srcH: Rules.Cell, tint: Color.White)
|
|
142
78
|
|
|
143
|
-
# A float in 0..1 from a seed.
|
|
144
|
-
fun
|
|
79
|
+
fun overlaps(ax: Float, ay: Float, ra: Float, bx: Float, by: Float, rb: Float) -> Bool =
|
|
145
|
-
|
|
80
|
+
Math.hypot(ax - bx, ay - by) < ra + rb
|
|
146
81
|
|
|
147
82
|
# A random spot at least `gap` away from (ax, ay), drifting at up to `speed`.
|
|
148
|
-
fun spawn(
|
|
83
|
+
fun spawn(rng: Random, ax: Float, ay: Float, gap: Float, speed: Float) -> Spawned =
|
|
149
84
|
margin := 30.0
|
|
150
|
-
|
|
85
|
+
r := rng
|
|
151
86
|
x := 0.0
|
|
152
87
|
y := 0.0
|
|
153
88
|
tries := 0
|
|
154
89
|
while tries < 20
|
|
155
|
-
|
|
90
|
+
dx := r.nextFloat()
|
|
91
|
+
dy := dx.rng.nextFloat()
|
|
92
|
+
r = dy.rng
|
|
156
|
-
x = margin +
|
|
93
|
+
x = margin + dx.value * {Rules.ArenaW - 2.0 * margin}
|
|
157
|
-
sd = nextSeed(sd)
|
|
158
|
-
y = margin +
|
|
94
|
+
y = margin + dy.value * {Rules.ArenaH - 2.0 * margin}
|
|
159
95
|
if Math.hypot(x - ax, y - ay) >= gap
|
|
160
96
|
break
|
|
161
97
|
tries = tries + 1
|
|
162
|
-
|
|
98
|
+
dh := r.nextFloat()
|
|
99
|
+
dp := dh.rng.nextFloat()
|
|
163
|
-
heading :=
|
|
100
|
+
heading := dh.value * Float.Tau
|
|
164
|
-
sd = nextSeed(sd)
|
|
165
|
-
pace := speed * {0.4 + 0.6 *
|
|
101
|
+
pace := speed * {0.4 + 0.6 * dp.value}
|
|
166
|
-
|
|
102
|
+
Spawned(at: Motion(x: x, y: y, vx: Math.cos(heading) * pace, vy: Math.sin(heading) * pace), rng: dp.rng)
|
|
167
103
|
|
|
168
104
|
# `b` moved for `dt` seconds and bounced off the arena walls; `b` itself is unchanged.
|
|
169
105
|
fun driftOf(b: Motion, r: Float, dt: Float) -> Motion =
|
|
170
|
-
|
|
106
|
+
x0 := b.x + b.vx * dt
|
|
171
|
-
|
|
107
|
+
y0 := b.y + b.vy * dt
|
|
172
|
-
vx := b.vx
|
|
173
|
-
vy := b.vy
|
|
174
|
-
if x < r || x > Rules.ArenaW - r
|
|
175
|
-
vx = 0.0 - b.vx
|
|
176
|
-
|
|
108
|
+
x := x0.max(r).min(Rules.ArenaW - r)
|
|
177
|
-
if y < r || y > Rules.ArenaH - r
|
|
178
|
-
vy = 0.0 - b.vy
|
|
179
|
-
|
|
109
|
+
y := y0.max(r).min(Rules.ArenaH - r)
|
|
180
|
-
Motion(x: x, y: y, vx: vx, vy: vy)
|
|
110
|
+
Motion(x: x, y: y, vx: if x != x0 then 0.0 - b.vx else b.vx, vy: if y != y0 then 0.0 - b.vy else b.vy)
|
|
181
111
|
|
|
182
112
|
# How many mines are out: more as the score grows.
|
|
183
113
|
fun mineCount(score: Int) -> Int =
|
|
184
114
|
{3 + score / 50}.min(8)
|
|
185
115
|
|
|
186
|
-
# Accelerates by the input axes, applies drag and keeps the ship in the arena.
|
|
187
|
-
fun shipStep(s: ShipState, ax: Float, ay: Float, dt: Float) -> ShipState =
|
|
188
|
-
keep := {1.0 - Rules.Drag * dt}.max(0.0)
|
|
189
|
-
vx := {s.vx + ax * Rules.Thrust * dt} * keep
|
|
190
|
-
vy := {s.vy + ay * Rules.Thrust * dt} * keep
|
|
191
|
-
x := s.x + vx * dt
|
|
192
|
-
y := s.y + vy * dt
|
|
193
|
-
r := Rules.ShipRadius
|
|
194
|
-
if x < r
|
|
195
|
-
x = r
|
|
196
|
-
vx = 0.0
|
|
197
|
-
if x > Rules.ArenaW - r
|
|
198
|
-
x = Rules.ArenaW - r
|
|
199
|
-
vx = 0.0
|
|
200
|
-
if y < r
|
|
201
|
-
y = r
|
|
202
|
-
vy = 0.0
|
|
203
|
-
if y > Rules.ArenaH - r
|
|
204
|
-
y = Rules.ArenaH - r
|
|
205
|
-
vy = 0.0
|
|
206
|
-
rot := s.rotation
|
|
207
|
-
if vx * vx + vy * vy > 400.0
|
|
208
|
-
rot = Math.atan2(vx, 0.0 - vy)
|
|
209
|
-
ShipState(x: x, y: y, vx: vx, vy: vy, rotation: rot, hurt: {s.hurt - dt}.max(0.0))
|
|
210
|
-
|
|
211
|
-
fun newShip() -> ShipState =
|
|
212
|
-
ShipState(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, rotation: 0.0, hurt: 0.0)
|
|
213
|
-
|
|
214
|
-
fun overlaps(ax: Float, ay: Float, ra: Float, bx: Float, by: Float, rb: Float) -> Bool =
|
|
215
|
-
Math.hypot(ax - bx, ay - by) < ra + rb
|
|
216
|
-
|
|
217
|
-
fun orbAt(id: Int, phase: Float, clock: Float, at: Motion) -> Orb =
|
|
218
|
-
Orb(id: id, x: at.x, y: at.y, vx: at.vx, vy: at.vy, phase: phase, clock: clock)
|
|
219
|
-
|
|
220
|
-
fun mineAt(id: Int, rotation: Float, spin: Float, clock: Float, at: Motion) -> Mine =
|
|
221
|
-
Mine(id: id, x: at.x, y: at.y, vx: at.vx, vy: at.vy, rotation: rotation, spin: spin, clock: clock)
|
|
222
|
-
|
|
223
|
-
fun orbMove(o: Orb, dt: Float) -> Orb =
|
|
224
|
-
m := driftOf(Motion(x: o.x, y: o.y, vx: o.vx, vy: o.vy), Rules.OrbRadius, dt)
|
|
225
|
-
orbAt(o.id, o.phase, o.clock + dt, m)
|
|
226
|
-
|
|
227
|
-
fun mineMove(k: Mine, dt: Float) -> Mine =
|
|
228
|
-
m := driftOf(Motion(x: k.x, y: k.y, vx: k.vx, vy: k.vy), Rules.MineRadius, dt)
|
|
229
|
-
mineAt(k.id, k.rotation + k.spin * dt, k.spin, k.clock + dt, m)
|
|
230
|
-
|
|
231
|
-
# --- the state ---
|
|
232
|
-
|
|
233
|
-
# A built list of orbs and the seed to continue from.
|
|
234
|
-
record OrbsMade(orbs: List[Orb], seed: Int)
|
|
235
|
-
|
|
236
|
-
record MinesMade(mines: List[Mine], seed: Int)
|
|
237
|
-
|
|
238
|
-
# Orbs `i` and up to OrbCount, each at a random spot away from the ship.
|
|
239
|
-
fun makeOrbs(i: Int, seed: Int, ship: ShipState) -> OrbsMade =
|
|
240
|
-
if i >= Rules.OrbCount
|
|
241
|
-
return OrbsMade(orbs: Nil, seed: seed)
|
|
242
|
-
sp := spawn(seed, ship.x, ship.y, 120.0, 40.0)
|
|
243
|
-
rest := makeOrbs(i + 1, sp.seed, ship)
|
|
244
|
-
OrbsMade(orbs: Cons(head: orbAt(Id.Orb + i, Float(i) * 0.1, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)), tail: rest.orbs), seed: rest.seed)
|
|
245
|
-
|
|
246
|
-
# Mines `i` and up to `count`.
|
|
247
|
-
fun makeMines(i: Int, count: Int, seed: Int, ship: ShipState) -> MinesMade =
|
|
248
|
-
if i >= count
|
|
249
|
-
return MinesMade(mines: Nil, seed: seed)
|
|
250
|
-
sp := spawn(seed, ship.x, ship.y, 220.0, 90.0)
|
|
251
|
-
rest := makeMines(i + 1, count, sp.seed, ship)
|
|
252
|
-
MinesMade(mines: Cons(head: mineAt(Id.Mine + i, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)), tail: rest.mines), seed: rest.seed)
|
|
253
|
-
|
|
254
|
-
fun newState(seed: Int, best: Int, sheet: Int, target: Int, sw: Float, sh: Float) -> State =
|
|
255
|
-
ship := newShip()
|
|
256
|
-
made := makeOrbs(0, seed, ship)
|
|
257
|
-
mm := makeMines(0, mineCount(0), made.seed, ship)
|
|
258
|
-
State(
|
|
259
|
-
ship: ship, orbs: made.orbs, mines: mm.mines, seed: mm.seed, score: 0, best: best, lives: Rules.StartLives,
|
|
260
|
-
over: false, clock: 0.0, overClock: 0.0, hurtAt: -1.0, overAt: -1.0, ui: Ui.init(), sw: sw, sh: sh, sheet: sheet, target: target)
|
|
261
|
-
|
|
262
|
-
fun orbStill(o: Orb) -> Orb =
|
|
263
|
-
orbAt(o.id, o.phase, o.clock, Motion(x: o.x, y: o.y, vx: 0.0, vy: 0.0))
|
|
264
|
-
|
|
265
|
-
fun mineStill(k: Mine) -> Mine =
|
|
266
|
-
mineAt(k.id, k.rotation, 0.0, k.clock, Motion(x: k.x, y: k.y, vx: 0.0, vy: 0.0))
|
|
267
|
-
|
|
268
|
-
# Stops every orb and mine: after game over they only animate.
|
|
269
|
-
fun freeze(s: State) -> State =
|
|
270
|
-
n := State(..s)
|
|
271
|
-
n.orbs = mapLink(s.orbs, |o| orbStill(o))
|
|
272
|
-
n.mines = mapLink(s.mines, |k| mineStill(k))
|
|
273
|
-
n
|
|
274
|
-
|
|
275
|
-
# The orbs after the ship collected the ones it touches, with the score and seed.
|
|
276
|
-
record Collected(orbs: List[Orb], seed: Int, score: Int)
|
|
277
|
-
|
|
278
|
-
fun collectOrbs(orbs: List[Orb], ship: ShipState, seed: Int, score: Int) -> Collected = match orbs
|
|
279
|
-
Nil => Collected(orbs: Nil, seed: seed, score: score)
|
|
280
|
-
Cons(o, rest) =>
|
|
281
|
-
if overlaps(ship.x, ship.y, Rules.ShipRadius, o.x, o.y, Rules.OrbRadius)
|
|
282
|
-
sp := spawn(seed, ship.x, ship.y, 120.0, 40.0)
|
|
283
|
-
later := collectOrbs(rest, ship, sp.seed, score + Rules.OrbPoints)
|
|
284
|
-
moved := orbAt(o.id, o.phase, o.clock, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy))
|
|
285
|
-
Collected(orbs: Cons(head: moved, tail: later.orbs), seed: later.seed, score: later.score)
|
|
286
|
-
else
|
|
287
|
-
kept := collectOrbs(rest, ship, seed, score)
|
|
288
|
-
Collected(orbs: Cons(head: o, tail: kept.orbs), seed: kept.seed, score: kept.score)
|
|
289
|
-
|
|
290
|
-
# `mines` grown (new mines go at the end) until there are `want`.
|
|
291
|
-
fun growMines(mines: List[Mine], have: Int, want: Int, seed: Int, ship: ShipState) -> MinesMade =
|
|
292
|
-
if have >= want
|
|
293
|
-
return MinesMade(mines: mines, seed: seed)
|
|
294
|
-
sp := spawn(seed, ship.x, ship.y, 220.0, 90.0)
|
|
295
|
-
added := snoc(mines, mineAt(Id.Mine + have, 0.0, 2.0, 0.0, Motion(x: sp.x, y: sp.y, vx: sp.vx, vy: sp.vy)))
|
|
296
|
-
growMines(added, have + 1, want, sp.seed, ship)
|
|
297
|
-
|
|
298
|
-
fun touchesMine(mines: List[Mine], ship: ShipState) -> Bool = match mines
|
|
299
|
-
Nil => false
|
|
300
|
-
Cons(k, rest) => overlaps(ship.x, ship.y, Rules.ShipRadius, k.x, k.y, Rules.MineRadius) || touchesMine(rest, ship)
|
|
301
|
-
|
|
302
|
-
# One frame of time. Once the game is over the actors only animate.
|
|
303
|
-
fun tick(s: State, dt: Float, ax: Float, ay: Float) -> State =
|
|
304
|
-
n := State(..s)
|
|
305
|
-
n.clock = s.clock + dt
|
|
306
|
-
n.orbs = mapLink(s.orbs, |o| orbMove(o, dt))
|
|
307
|
-
n.mines = mapLink(s.mines, |k| mineMove(k, dt))
|
|
308
|
-
if s.over
|
|
309
|
-
n.overClock = s.overClock + dt
|
|
310
|
-
return n
|
|
311
|
-
ship := shipStep(s.ship, ax, ay, dt)
|
|
312
|
-
n.ship = ship
|
|
313
|
-
got := collectOrbs(n.orbs, ship, n.seed, n.score)
|
|
314
|
-
n.orbs = got.orbs
|
|
315
|
-
n.seed = got.seed
|
|
316
|
-
n.score = got.score
|
|
317
|
-
grown := growMines(n.mines, lenLink(n.mines), mineCount(n.score), n.seed, ship)
|
|
318
|
-
n.mines = grown.mines
|
|
319
|
-
n.seed = grown.seed
|
|
320
|
-
if n.ship.hurt <= 0.0 && touchesMine(n.mines, ship)
|
|
321
|
-
n.lives = n.lives - 1
|
|
322
|
-
n.ship = ShipState(x: ship.x, y: ship.y, vx: ship.vx, vy: ship.vy, rotation: ship.rotation, hurt: Rules.HurtTime)
|
|
323
|
-
n.hurtAt = n.clock
|
|
324
|
-
if n.lives <= 0
|
|
325
|
-
n.over = true
|
|
326
|
-
n.overAt = n.clock
|
|
327
|
-
n.best = n.best.max(n.score)
|
|
328
|
-
if n.over
|
|
329
|
-
return freeze(n)
|
|
330
|
-
n
|
|
331
|
-
|
|
332
|
-
# The next state: pure, nothing that is passed in is changed.
|
|
333
|
-
fun step(s: State, msg: Msg) -> State = match msg
|
|
334
|
-
Tick(dt, ax, ay) => tick(s, dt, ax, ay)
|
|
335
|
-
Resize(w, h) => resized(s, w, h)
|
|
336
|
-
Restart(seed) => restarted(s, seed)
|
|
337
|
-
UiEv(event) => uiEvent(s, event)
|
|
338
|
-
|
|
339
|
-
fun restarted(s: State, seed: Int) -> State =
|
|
340
|
-
n := newState(seed, s.best.max(s.score), s.sheet, s.target, s.sw, s.sh)
|
|
341
|
-
n.ui = s.ui
|
|
342
|
-
n
|
|
343
|
-
|
|
344
|
-
# A click on the restart button restarts a finished game; other events change nothing.
|
|
345
|
-
fun uiEvent(s: State, e: UiEvent) -> State =
|
|
346
|
-
if s.over && e.id == Id.Restart && e.kind == Click
|
|
347
|
-
return restarted(s, nextSeed(s.seed))
|
|
348
|
-
s
|
|
349
|
-
|
|
350
|
-
fun resized(s: State, w: Float, h: Float) -> State =
|
|
351
|
-
n := State(..s)
|
|
352
|
-
n.sw = w
|
|
353
|
-
n.sh = h
|
|
354
|
-
n
|
|
355
|
-
|
|
356
|
-
# The zoom that fits the arena into the screen, and where the arena's corner lands.
|
|
357
|
-
fun zoomOf(s: State) -> Float =
|
|
358
|
-
{s.sw / Rules.ArenaW}.min(s.sh / Rules.ArenaH)
|
|
359
|
-
|
|
360
|
-
fun offsetX(s: State) -> Float =
|
|
361
|
-
{s.sw - zoomOf(s) * Rules.ArenaW} / 2.0
|
|
362
|
-
|
|
363
|
-
fun offsetY(s: State) -> Float =
|
|
364
|
-
{s.sh - zoomOf(s) * Rules.ArenaH} / 2.0
|
|
365
|
-
|
|
366
|
-
# --- the view ---
|
|
367
|
-
|
|
368
|
-
fun cell(sheet: Int, x: Float, y: Float, size: Float, col: Int, row: Int) -> SceneNode =
|
|
369
|
-
Sprite(
|
|
370
|
-
tex: sheet, x: x, y: y, w: size, h: size, srcX: Float(col) * Rules.Cell, srcY: Float(row) * Rules.Cell,
|
|
371
|
-
srcW: Rules.Cell, srcH: Rules.Cell, tint: Color.White)
|
|
372
|
-
|
|
373
|
-
fun starView(i: Int, clock: Float) -> SceneNode =
|
|
374
|
-
r := 1.0 + Float(i % 3) * 0.5
|
|
375
|
-
twinkle := 0.25 + 0.2 * Math.sin(clock * 2.0 + Float(i))
|
|
376
|
-
CircleShape(cx: Float({i * 7919} % 800), cy: Float({i * 104729} % 600), r: r, color: Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
|
|
377
|
-
|
|
378
|
-
# The stars `i` and up to 40.
|
|
379
|
-
fun starNodes(i: Int, clock: Float) -> List[SceneNode] =
|
|
380
|
-
if i >= 40 then Nil else Cons(head: starView(i, clock), tail: starNodes(i + 1, clock))
|
|
381
|
-
|
|
382
|
-
fun starsView(clock: Float) -> SceneNode =
|
|
383
|
-
Group(transform: Identity, children: starNodes(0, clock))
|
|
384
|
-
|
|
385
|
-
fun mineView(sheet: Int, k: Mine) -> SceneNode =
|
|
386
|
-
Group(
|
|
387
|
-
transform: Compose(first: Translate(dx: k.x, dy: k.y), then: Rotate(radians: k.rotation)),
|
|
388
|
-
children: Cons(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(k.clock, 4, 4.0), 2), tail: Nil))
|
|
389
|
-
|
|
390
|
-
# The glow is drawn additively, then the sprite on top.
|
|
391
|
-
fun orbView(sheet: Int, o: Orb) -> SceneNode =
|
|
392
|
-
glow := Group(
|
|
393
|
-
transform: Identity,
|
|
394
|
-
children: Cons(
|
|
395
|
-
head: CircleShape(cx: 0.0, cy: 0.0, r: 26.0, color: Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5)),
|
|
396
|
-
tail: Cons(head: CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5)), tail: Nil)))
|
|
397
|
-
Group(
|
|
398
|
-
transform: Translate(dx: o.x, dy: o.y),
|
|
399
|
-
children: Cons(
|
|
400
|
-
head: Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
|
|
401
|
-
tail: Cons(head: cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(o.clock + o.phase, 4, 8.0), 1), tail: Nil)))
|
|
402
|
-
|
|
403
116
|
# The hurt flash: the ship shrinks and recovers every 0.1 s (the scale returns to 1 each lap).
|
|
404
117
|
fun hitFlash() -> Anim =
|
|
405
118
|
Forever(anim: Seq(steps: List(ScaleTo(sx: 0.4, sy: 0.4, secs: 0.05), ScaleTo(sx: 2.5, sy: 2.5, secs: 0.05))))
|
|
@@ -416,85 +129,297 @@ fun titleAnim() -> Anim =
|
|
|
416
129
|
fun titlePose(t: Float) -> AnimPose =
|
|
417
130
|
Anim.at(titleAnim(), t)
|
|
418
131
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
fun
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
132
|
+
# --- the components ---
|
|
133
|
+
|
|
134
|
+
# The player.
|
|
135
|
+
record Ship(x: Float, y: Float, vx: Float, vy: Float, rotation: Float, hurt: Float) =
|
|
136
|
+
|
|
137
|
+
fun start() -> Ship =
|
|
138
|
+
Ship(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, rotation: 0.0, hurt: 0.0)
|
|
139
|
+
|
|
140
|
+
# Accelerates by the input axes, applies drag and keeps the ship in the arena.
|
|
141
|
+
fun update(self, ax: Float, ay: Float, dt: Float) -> Ship =
|
|
142
|
+
keep := {1.0 - Rules.Drag * dt}.max(0.0)
|
|
143
|
+
r := Rules.ShipRadius
|
|
144
|
+
vx0 := {self.vx + ax * Rules.Thrust * dt} * keep
|
|
145
|
+
vy0 := {self.vy + ay * Rules.Thrust * dt} * keep
|
|
146
|
+
x0 := self.x + vx0 * dt
|
|
147
|
+
y0 := self.y + vy0 * dt
|
|
148
|
+
x := x0.max(r).min(Rules.ArenaW - r)
|
|
149
|
+
y := y0.max(r).min(Rules.ArenaH - r)
|
|
150
|
+
vx := if x != x0 then 0.0 else vx0
|
|
151
|
+
vy := if y != y0 then 0.0 else vy0
|
|
152
|
+
Ship(
|
|
153
|
+
x: x, y: y, vx: vx, vy: vy,
|
|
154
|
+
rotation: if vx * vx + vy * vy > 400.0 then Math.atan2(vx, 0.0 - vy) else self.rotation,
|
|
155
|
+
hurt: {self.hurt - dt}.max(0.0))
|
|
156
|
+
|
|
157
|
+
fun touches(self, at: Motion, radius: Float) -> Bool =
|
|
158
|
+
overlaps(self.x, self.y, Rules.ShipRadius, at.x, at.y, radius)
|
|
159
|
+
|
|
160
|
+
# The ship blinks while hurt (`hurtAge` seconds since the hit).
|
|
161
|
+
fun render(self, sheet: Int, clock: Float, hurtAge: Float) -> SceneNode =
|
|
162
|
+
blink := self.hurt > 0.0 && flashPose(hurtAge).scaleX < 0.7
|
|
163
|
+
if blink
|
|
164
|
+
nothing()
|
|
165
|
+
else
|
|
166
|
+
Group(
|
|
167
|
+
transform: Compose(first: Translate(dx: self.x, dy: self.y), then: Rotate(radians: self.rotation)),
|
|
168
|
+
children: List(
|
|
169
|
+
CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12)),
|
|
170
|
+
cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(clock, 4, 14.0), 0)))
|
|
171
|
+
|
|
172
|
+
# A glowing orb drifting about. `phase` offsets its animation from the other orbs.
|
|
173
|
+
record Orb(id: Int, at: Motion, phase: Float, clock: Float) =
|
|
174
|
+
|
|
175
|
+
fun update(self, dt: Float) -> Orb =
|
|
176
|
+
Orb(..self, at: driftOf(self.at, Rules.OrbRadius, dt), clock: self.clock + dt)
|
|
177
|
+
|
|
178
|
+
fun moved(self, to: Motion) -> Orb =
|
|
179
|
+
Orb(..self, at: to)
|
|
180
|
+
|
|
181
|
+
fun still(self) -> Orb =
|
|
182
|
+
p := self.at
|
|
183
|
+
Orb(..self, at: Motion(..p, vx: 0.0, vy: 0.0))
|
|
184
|
+
|
|
185
|
+
# The glow is drawn additively, then the sprite on top.
|
|
186
|
+
fun render(self, sheet: Int) -> SceneNode =
|
|
187
|
+
glow := Group(
|
|
188
|
+
transform: Identity,
|
|
189
|
+
children: List(
|
|
190
|
+
CircleShape(cx: 0.0, cy: 0.0, r: 26.0, color: Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5)),
|
|
191
|
+
CircleShape(cx: 0.0, cy: 0.0, r: 17.0, color: Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5))))
|
|
192
|
+
Group(
|
|
193
|
+
transform: Translate(dx: self.at.x, dy: self.at.y),
|
|
194
|
+
children: List(
|
|
195
|
+
Filter(effect: BlendFx(mode: Blend.Add), child: glow, target: 0 - 1),
|
|
196
|
+
cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(self.clock + self.phase, 4, 8.0), 1)))
|
|
197
|
+
|
|
198
|
+
# A spinning mine drifting about.
|
|
199
|
+
record Mine(id: Int, at: Motion, rotation: Float, spin: Float, clock: Float) =
|
|
200
|
+
|
|
201
|
+
fun update(self, dt: Float) -> Mine =
|
|
202
|
+
Mine(..self, at: driftOf(self.at, Rules.MineRadius, dt), rotation: self.rotation + self.spin * dt, clock: self.clock + dt)
|
|
203
|
+
|
|
204
|
+
fun moved(self, to: Motion) -> Mine =
|
|
205
|
+
Mine(..self, at: to)
|
|
206
|
+
|
|
207
|
+
fun still(self) -> Mine =
|
|
208
|
+
p := self.at
|
|
209
|
+
Mine(..self, at: Motion(..p, vx: 0.0, vy: 0.0), spin: 0.0)
|
|
210
|
+
|
|
211
|
+
fun render(self, sheet: Int) -> SceneNode =
|
|
212
|
+
Group(
|
|
213
|
+
transform: Compose(first: Translate(dx: self.at.x, dy: self.at.y), then: Rotate(radians: self.rotation)),
|
|
214
|
+
children: List(cell(sheet, 0.0 - Rules.Half, 0.0 - Rules.Half, Rules.Size, frameOf(self.clock, 4, 4.0), 2)))
|
|
215
|
+
|
|
216
|
+
# A twinkling background star; it never moves.
|
|
217
|
+
record Star(i: Int) =
|
|
218
|
+
|
|
219
|
+
# Stars `i` and up to 40.
|
|
220
|
+
fun field(i: Int) -> List[Star] =
|
|
221
|
+
if i >= 40 then Nil else Cons(head: Star(i: i), tail: Star.field(i + 1))
|
|
222
|
+
|
|
223
|
+
fun render(self, clock: Float) -> SceneNode =
|
|
224
|
+
r := 1.0 + Float(self.i % 3) * 0.5
|
|
225
|
+
twinkle := 0.25 + 0.2 * Math.sin(clock * 2.0 + Float(self.i))
|
|
226
|
+
CircleShape(cx: Float({self.i * 7919} % 800), cy: Float({self.i * 104729} % 600), r: r, color: Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
|
|
227
|
+
|
|
228
|
+
# What can happen to the state.
|
|
229
|
+
enum Msg =
|
|
230
|
+
| Tick(dt: Float, ax: Float, ay: Float)
|
|
231
|
+
| Resize(w: Float, h: Float)
|
|
232
|
+
| Restart(seed: Int)
|
|
233
|
+
| UiEv(event: UiEvent)
|
|
234
|
+
|
|
235
|
+
# Everything that changes during a run. `sheet` and `target` are resource handles
|
|
236
|
+
# the picture needs: the sprite sheet and the render target of the game-over filter.
|
|
237
|
+
record State(
|
|
238
|
+
ship: Ship,
|
|
239
|
+
orbs: List[Orb],
|
|
240
|
+
mines: List[Mine],
|
|
241
|
+
stars: List[Star],
|
|
242
|
+
rng: Random,
|
|
243
|
+
score: Int,
|
|
244
|
+
best: Int,
|
|
245
|
+
lives: Int,
|
|
246
|
+
over: Bool,
|
|
247
|
+
clock: Float,
|
|
248
|
+
overClock: Float,
|
|
249
|
+
hurtAt: Float,
|
|
250
|
+
overAt: Float,
|
|
251
|
+
ui: UiState,
|
|
252
|
+
sw: Float,
|
|
253
|
+
sh: Float,
|
|
254
|
+
sheet: Int,
|
|
255
|
+
target: Int) =
|
|
256
|
+
|
|
257
|
+
# A fresh run: the orbs and the mines spread over the arena away from the ship.
|
|
258
|
+
fun start(seed: Int, best: Int, sheet: Int, target: Int, sw: Float, sh: Float) -> State =
|
|
259
|
+
State(
|
|
260
|
+
ship: Ship.start(), orbs: Nil, mines: Nil, stars: Star.field(0), rng: Random.seeded(seed), score: 0, best: best,
|
|
261
|
+
lives: Rules.StartLives, over: false, clock: 0.0, overClock: 0.0, hurtAt: -1.0, overAt: -1.0, ui: Ui.init(),
|
|
262
|
+
sw: sw, sh: sh, sheet: sheet, target: target).fillOrbs(0).growMines()
|
|
263
|
+
|
|
264
|
+
# --- update ---
|
|
265
|
+
|
|
266
|
+
fun update(self, msg: Msg) -> State = match msg
|
|
267
|
+
Tick(dt, ax, ay) => self.tick(dt, ax, ay)
|
|
268
|
+
Resize(w, h) => State(..self, sw: w, sh: h)
|
|
269
|
+
Restart(seed) => self.restarted(seed)
|
|
270
|
+
UiEv(event) => self.onUi(event)
|
|
271
|
+
|
|
272
|
+
# One frame of time. Once the game is over the actors only animate.
|
|
273
|
+
fun tick(self, dt: Float, ax: Float, ay: Float) -> State =
|
|
274
|
+
moved := self.moveAll(dt)
|
|
275
|
+
if self.over
|
|
276
|
+
State(..moved, overClock: self.overClock + dt)
|
|
277
|
+
else
|
|
278
|
+
moved.sail(ax, ay, dt).collect().growMines().hurt().settle()
|
|
279
|
+
|
|
280
|
+
fun moveAll(self, dt: Float) -> State =
|
|
281
|
+
State(..self, clock: self.clock + dt, orbs: self.orbs.map(|o| o.update(dt)), mines: self.mines.map(|k| k.update(dt)))
|
|
282
|
+
|
|
283
|
+
fun sail(self, ax: Float, ay: Float, dt: Float) -> State =
|
|
284
|
+
State(..self, ship: self.ship.update(ax, ay, dt))
|
|
285
|
+
|
|
286
|
+
# The ship collects every orb it touches.
|
|
287
|
+
fun collect(self) -> State =
|
|
288
|
+
self.orbs.fold(self, |s, o| s.takeOrb(o))
|
|
289
|
+
|
|
290
|
+
fun takeOrb(self, o: Orb) -> State =
|
|
291
|
+
if self.ship.touches(o.at, Rules.OrbRadius) then self.respawnOrb(o) else self
|
|
292
|
+
|
|
293
|
+
# Scores `o` and puts it at a new random spot.
|
|
294
|
+
fun respawnOrb(self, o: Orb) -> State =
|
|
295
|
+
sp := spawn(self.rng, self.ship.x, self.ship.y, 120.0, 40.0)
|
|
296
|
+
State(
|
|
297
|
+
..self, orbs: self.orbs.map(|x| if x.id == o.id then x.moved(sp.at) else x), rng: sp.rng,
|
|
298
|
+
score: self.score + Rules.OrbPoints)
|
|
299
|
+
|
|
300
|
+
# Orbs `i` and up to OrbCount, each at a random spot away from the ship.
|
|
301
|
+
fun fillOrbs(self, i: Int) -> State =
|
|
302
|
+
if i >= Rules.OrbCount then self else self.addOrb(i).fillOrbs(i + 1)
|
|
303
|
+
|
|
304
|
+
fun addOrb(self, i: Int) -> State =
|
|
305
|
+
sp := spawn(self.rng, self.ship.x, self.ship.y, 120.0, 40.0)
|
|
306
|
+
State(
|
|
307
|
+
..self, orbs: self.orbs.append(List(Orb(id: Id.Orb + i, at: sp.at, phase: Float(i) * 0.1, clock: 0.0))),
|
|
308
|
+
rng: sp.rng)
|
|
309
|
+
|
|
310
|
+
# Mines are added (at the end) until there are as many as the score asks for.
|
|
311
|
+
fun growMines(self) -> State =
|
|
312
|
+
if self.mines.length() >= mineCount(self.score) then self else self.addMine().growMines()
|
|
313
|
+
|
|
314
|
+
fun addMine(self) -> State =
|
|
315
|
+
sp := spawn(self.rng, self.ship.x, self.ship.y, 220.0, 90.0)
|
|
316
|
+
State(
|
|
317
|
+
..self,
|
|
318
|
+
mines: self.mines.append(List(Mine(id: Id.Mine + self.mines.length(), at: sp.at, rotation: 0.0, spin: 2.0, clock: 0.0))),
|
|
319
|
+
rng: sp.rng)
|
|
320
|
+
|
|
321
|
+
fun touchesMine(self) -> Bool =
|
|
322
|
+
self.mines.fold(false, |hit, k| hit || self.ship.touches(k.at, Rules.MineRadius))
|
|
323
|
+
|
|
324
|
+
# A mine costs a life, makes the ship flash and, at the last life, ends the game.
|
|
325
|
+
fun hurt(self) -> State =
|
|
326
|
+
if self.ship.hurt <= 0.0 && self.touchesMine() then self.struck() else self
|
|
327
|
+
|
|
328
|
+
fun struck(self) -> State =
|
|
329
|
+
lives := self.lives - 1
|
|
330
|
+
done := lives <= 0
|
|
331
|
+
ship := self.ship
|
|
332
|
+
State(
|
|
333
|
+
..self, lives: lives, ship: Ship(..ship, hurt: Rules.HurtTime), hurtAt: self.clock, over: done,
|
|
334
|
+
overAt: if done then self.clock else self.overAt, best: if done then self.best.max(self.score) else self.best)
|
|
335
|
+
|
|
336
|
+
# After game over the orbs and mines stop: they only animate.
|
|
337
|
+
fun settle(self) -> State =
|
|
338
|
+
if self.over then State(..self, orbs: self.orbs.map(|o| o.still()), mines: self.mines.map(|k| k.still())) else self
|
|
339
|
+
|
|
340
|
+
fun restarted(self, seed: Int) -> State =
|
|
341
|
+
fresh := State.start(seed, self.best.max(self.score), self.sheet, self.target, self.sw, self.sh)
|
|
342
|
+
State(..fresh, ui: self.ui)
|
|
343
|
+
|
|
344
|
+
# A click on the restart button restarts a finished game; other events change nothing.
|
|
345
|
+
fun onUi(self, e: UiEvent) -> State =
|
|
346
|
+
if self.over && e.id == Id.Restart && e.kind == Click then self.restarted(self.rng.nextInt().value) else self
|
|
347
|
+
|
|
348
|
+
# One pointer input: finds what is under it in the current picture, runs the Ui, and
|
|
349
|
+
# feeds each raised event to `update`.
|
|
350
|
+
fun feed(self, input: UiInput) -> State =
|
|
351
|
+
r := Ui.step(self.ui, input, Picture.hitId(self.render(), input.x, input.y))
|
|
352
|
+
r.events.fold(State(..self, ui: r.ui), |s, e| s.update(UiEv(event: e)))
|
|
353
|
+
|
|
354
|
+
# --- render ---
|
|
355
|
+
|
|
356
|
+
# The zoom that fits the arena into the screen, and where the arena's corner lands.
|
|
357
|
+
fun zoom(self) -> Float =
|
|
358
|
+
{self.sw / Rules.ArenaW}.min(self.sh / Rules.ArenaH)
|
|
359
|
+
|
|
360
|
+
fun offsetX(self) -> Float =
|
|
361
|
+
{self.sw - self.zoom() * Rules.ArenaW} / 2.0
|
|
362
|
+
|
|
363
|
+
fun offsetY(self) -> Float =
|
|
364
|
+
{self.sh - self.zoom() * Rules.ArenaH} / 2.0
|
|
365
|
+
|
|
366
|
+
# The whole picture, in drawing order, under the camera that fits the arena on screen.
|
|
367
|
+
fun render(self) -> SceneNode =
|
|
368
|
+
z := self.zoom()
|
|
369
|
+
Group(
|
|
370
|
+
transform: Compose(first: Translate(dx: self.offsetX(), dy: self.offsetY()), then: Scale(sx: z, sy: z)),
|
|
371
|
+
children: List(
|
|
372
|
+
RectShape(x: 0.0, y: 0.0, w: Rules.ArenaW, h: Rules.ArenaH, color: Color.rgb(0.07, 0.08, 0.16)),
|
|
373
|
+
Group(transform: Identity, children: self.stars.map(|st| st.render(self.clock))),
|
|
374
|
+
Group(transform: Identity, children: self.mines.map(|k| k.render(self.sheet))),
|
|
375
|
+
Group(transform: Identity, children: self.orbs.map(|o| o.render(self.sheet))),
|
|
376
|
+
self.renderShip(),
|
|
377
|
+
self.renderHud(),
|
|
378
|
+
self.renderOverlay()))
|
|
379
|
+
|
|
380
|
+
# The ship is gone once the game is over (an empty group keeps the slots).
|
|
381
|
+
fun renderShip(self) -> SceneNode =
|
|
382
|
+
if self.over then nothing() else self.ship.render(self.sheet, self.clock, self.clock - self.hurtAt)
|
|
383
|
+
|
|
384
|
+
# One life icon per remaining life, from the right edge going left.
|
|
385
|
+
fun lifeIcons(self, i: Int) -> List[SceneNode] =
|
|
386
|
+
if i >= self.lives
|
|
387
|
+
Nil
|
|
388
|
+
else
|
|
389
|
+
Cons(head: cell(self.sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 0, 0), tail: self.lifeIcons(i + 1))
|
|
390
|
+
|
|
391
|
+
fun renderHud(self) -> SceneNode =
|
|
392
|
+
Group(
|
|
393
|
+
transform: Identity,
|
|
394
|
+
children: Cons(
|
|
395
|
+
head: Text(text: "Score {self.score}", x: 16.0, y: 12.0, size: 28.0, color: Color.White),
|
|
396
|
+
tail: Cons(head: Text(text: "Best {self.best.max(self.score)}", x: 16.0, y: 44.0, size: 20.0, color: Color.Yellow), tail: self.lifeIcons(0))))
|
|
397
|
+
|
|
398
|
+
fun renderTitle(self) -> SceneNode =
|
|
399
|
+
p := titlePose(self.clock - self.overAt)
|
|
400
|
+
Group(
|
|
401
|
+
transform: Compose(first: Translate(dx: 400.0 + p.dx, dy: 268.0 + p.dy), then: Scale(sx: p.scaleX, sy: p.scaleY)),
|
|
402
|
+
children: List(Text(text: "Game over", x: -100.0, y: -24.0, size: 48.0, color: Color.Red)))
|
|
403
|
+
|
|
404
|
+
# The game-over panel is empty while playing, so nothing in it can be hit.
|
|
405
|
+
fun renderOverlay(self) -> SceneNode =
|
|
406
|
+
if self.over then self.renderPanel() else nothing()
|
|
407
|
+
|
|
408
|
+
# The panel with the restart button, faded in through a render-target filter.
|
|
409
|
+
fun renderPanel(self) -> SceneNode =
|
|
410
|
+
button := Ui.button(Id.Restart, "Again", 340.0, 342.0, 120.0, 44.0, self.ui)
|
|
411
|
+
panel := Group(
|
|
412
|
+
transform: Identity,
|
|
413
|
+
children: List(
|
|
414
|
+
RectShape(x: 0.0, y: 230.0, w: Rules.ArenaW, h: 170.0, color: Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
415
|
+
self.renderTitle(),
|
|
416
|
+
Text(text: "Press Enter to play again", x: 270.0, y: 300.0, size: 24.0, color: Color.White),
|
|
417
|
+
button))
|
|
418
|
+
Filter(effect: Alpha(alpha: {self.overClock * 2.0}.min(1.0)), child: panel, target: self.target)
|
|
419
|
+
|
|
420
|
+
# True when (x, y) in screen pixels is on the restart button of the picture `render` builds.
|
|
421
|
+
fun onRestart(self, x: Float, y: Float) -> Bool =
|
|
422
|
+
{Picture.hitId(self.render(), x, y) ?: -1} == Id.Restart
|
|
498
423
|
|
|
499
424
|
fun pointerKind(down: Bool, wasDown: Bool) -> UiInputKind =
|
|
500
425
|
if down && !wasDown
|
|
@@ -506,22 +431,22 @@ fun pointerKind(down: Bool, wasDown: Bool) -> UiInputKind =
|
|
|
506
431
|
|
|
507
432
|
# --- the host shell ---
|
|
508
433
|
|
|
509
|
-
record World(state: State, wasDown: Bool, pickupSound: Int, hitSound: Int, overSound: Int, musicSound: Int)
|
|
434
|
+
record World(mut state: State, mut wasDown: Bool, pickupSound: Int, hitSound: Int, overSound: Int, musicSound: Int)
|
|
510
435
|
|
|
511
436
|
fun musicVolume() -> Float =
|
|
512
437
|
0.3
|
|
513
438
|
|
|
514
439
|
# Restarts the music loop from the top.
|
|
515
|
-
fun startMusic(w: World) =
|
|
440
|
+
fun startMusic(io: Io, w: World) =
|
|
516
|
-
soundStop(w.musicSound)
|
|
441
|
+
io.soundStop(w.musicSound)
|
|
517
|
-
soundLoop(w.musicSound, musicVolume())
|
|
442
|
+
io.soundLoop(w.musicSound, musicVolume())
|
|
518
443
|
|
|
519
|
-
export fun init() -> World =
|
|
444
|
+
export fun init(io: Io) -> World =
|
|
520
|
-
s :=
|
|
445
|
+
s := State.start(io.randomSeed(), 0, io.textureLoad("sprites.png"), io.textureCreate(1600, 1200), Rules.ArenaW, Rules.ArenaH)
|
|
521
446
|
w := World(
|
|
522
|
-
state: s, wasDown: false, pickupSound: soundLoad("pickup.wav"), hitSound: soundLoad("hit.wav"),
|
|
447
|
+
state: s, wasDown: false, pickupSound: io.soundLoad("pickup.wav"), hitSound: io.soundLoad("hit.wav"),
|
|
523
|
-
overSound: soundLoad("gameover.wav"), musicSound: soundLoad("music.wav"))
|
|
448
|
+
overSound: io.soundLoad("gameover.wav"), musicSound: io.soundLoad("music.wav"))
|
|
524
|
-
startMusic(w)
|
|
449
|
+
startMusic(io, w)
|
|
525
450
|
w
|
|
526
451
|
|
|
527
452
|
fun axis(negative: Bool, positive: Bool) -> Float =
|
|
@@ -532,39 +457,39 @@ fun axis(negative: Bool, positive: Bool) -> Float =
|
|
|
532
457
|
else
|
|
533
458
|
0.0
|
|
534
459
|
|
|
535
|
-
export fun update(w: World, dt: Float) =
|
|
460
|
+
export fun update(io: Io, w: World, dt: Float) =
|
|
536
|
-
s :=
|
|
461
|
+
s := w.state.update(Resize(w: io.screenWidth(), h: io.screenHeight()))
|
|
537
|
-
down := isMouseDown(0)
|
|
462
|
+
down := io.isMouseDown(0)
|
|
538
|
-
m := mousePos()
|
|
463
|
+
m := io.mousePos()
|
|
539
|
-
clicked := feed(
|
|
464
|
+
clicked := s.feed(UiInput(kind: pointerKind(down, w.wasDown), x: m.x, y: m.y))
|
|
540
465
|
w.wasDown = down
|
|
541
466
|
if s.over && !clicked.over
|
|
542
467
|
w.state = clicked
|
|
543
|
-
startMusic(w)
|
|
468
|
+
startMusic(io, w)
|
|
544
469
|
return
|
|
545
|
-
s = clicked
|
|
546
|
-
if
|
|
470
|
+
if clicked.over && {io.isKeyPressed(Key.Enter) || io.isKeyPressed(Key.KeyR)}
|
|
547
|
-
w.state =
|
|
471
|
+
w.state = clicked.update(Restart(seed: clicked.rng.nextInt().value))
|
|
548
|
-
startMusic(w)
|
|
472
|
+
startMusic(io, w)
|
|
549
473
|
return
|
|
550
|
-
left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
|
|
474
|
+
left := io.isKeyDown(Key.Left) || io.isKeyDown(Key.KeyA)
|
|
551
|
-
right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
|
|
475
|
+
right := io.isKeyDown(Key.Right) || io.isKeyDown(Key.KeyD)
|
|
552
|
-
up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
|
|
476
|
+
up := io.isKeyDown(Key.Up) || io.isKeyDown(Key.KeyW)
|
|
553
|
-
downKey := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
|
|
477
|
+
downKey := io.isKeyDown(Key.Down) || io.isKeyDown(Key.KeyS)
|
|
554
|
-
t :=
|
|
478
|
+
t := clicked.update(Tick(dt: dt, ax: axis(left, right), ay: axis(up, downKey)))
|
|
555
|
-
if t.score >
|
|
479
|
+
if t.score > clicked.score
|
|
556
|
-
soundPlay(w.pickupSound, 0.8)
|
|
480
|
+
io.soundPlay(w.pickupSound, 0.8)
|
|
557
|
-
if t.lives <
|
|
481
|
+
if t.lives < clicked.lives
|
|
558
482
|
if t.over
|
|
559
|
-
soundStop(w.musicSound)
|
|
483
|
+
io.soundStop(w.musicSound)
|
|
560
|
-
soundPlay(w.overSound, 1.0)
|
|
484
|
+
io.soundPlay(w.overSound, 1.0)
|
|
561
485
|
else
|
|
562
|
-
soundPlay(w.hitSound, 1.0)
|
|
486
|
+
io.soundPlay(w.hitSound, 1.0)
|
|
563
487
|
w.state = t
|
|
564
488
|
|
|
565
|
-
export fun draw(w: World) =
|
|
489
|
+
export fun draw(io: Io, w: World) =
|
|
490
|
+
s := w.state
|
|
566
|
-
clear(Color.rgb(0.04, 0.05, 0.1))
|
|
491
|
+
io.clear(Color.rgb(0.04, 0.05, 0.1))
|
|
567
|
-
Picture.draw(
|
|
492
|
+
Picture.draw(io, s.render())
|
|
568
493
|
|
|
569
494
|
# --- tests ---
|
|
570
495
|
|
|
@@ -572,46 +497,30 @@ fun near(a: Float, b: Float) -> Bool =
|
|
|
572
497
|
{a - b}.abs() < 0.01
|
|
573
498
|
|
|
574
499
|
fun tickOf(s: State, ax: Float, ay: Float, dt: Float) -> State =
|
|
575
|
-
|
|
500
|
+
s.update(Tick(dt: dt, ax: ax, ay: ay))
|
|
576
501
|
|
|
577
502
|
fun away() -> Motion =
|
|
578
503
|
Motion(x: 5000.0, y: 0.0, vx: 0.0, vy: 0.0)
|
|
579
504
|
|
|
505
|
+
fun newState(seed: Int) -> State =
|
|
506
|
+
State.start(seed, 0, -1, -1, Rules.ArenaW, Rules.ArenaH)
|
|
507
|
+
|
|
580
508
|
fun withOrb(s: State, i: Int, m: Motion) -> State =
|
|
581
|
-
n := State(..s)
|
|
582
|
-
|
|
509
|
+
State(..s, orbs: s.orbs.map(|o| if o.id == Id.Orb + i then Orb(id: o.id, at: m, phase: Float(i) * 0.1, clock: 0.0) else o))
|
|
583
|
-
n
|
|
584
510
|
|
|
585
511
|
fun withMine(s: State, i: Int, m: Motion) -> State =
|
|
586
|
-
n := State(..s)
|
|
587
|
-
|
|
512
|
+
State(..s, mines: s.mines.map(|k| if k.id == Id.Mine + i then Mine(id: k.id, at: m, rotation: 0.0, spin: 2.0, clock: 0.0) else k))
|
|
588
|
-
n
|
|
589
513
|
|
|
590
514
|
# A state with the orbs and mines moved out of the way.
|
|
591
515
|
fun emptyState() -> State =
|
|
592
|
-
s := newState(7, 0, -1, -1, Rules.ArenaW, Rules.ArenaH)
|
|
593
|
-
for i := range Rules.OrbCount
|
|
594
|
-
s = withOrb(s, i, away())
|
|
595
|
-
for i := range lenLink(s.mines)
|
|
596
|
-
|
|
516
|
+
s := newState(7)
|
|
597
|
-
s
|
|
598
|
-
|
|
599
|
-
fun orbN(s: State, i: Int) -> Orb =
|
|
600
|
-
nthOr(s.orbs, i, orbAt(0, 0.0, 0.0, away()))
|
|
601
|
-
|
|
602
|
-
fun mineN(s: State, i: Int) -> Mine =
|
|
603
|
-
|
|
517
|
+
State(..s, orbs: s.orbs.map(|o| o.moved(away())), mines: s.mines.map(|k| k.moved(away())))
|
|
604
518
|
|
|
605
519
|
fun onShip(s: State) -> Motion =
|
|
606
520
|
Motion(x: s.ship.x, y: s.ship.y, vx: 0.0, vy: 0.0)
|
|
607
521
|
|
|
608
522
|
fun forceOver(s: State) -> State =
|
|
609
|
-
n := State(..s)
|
|
610
|
-
n.lives = 0
|
|
611
|
-
n.over = true
|
|
612
|
-
n.overAt = s.clock
|
|
613
|
-
|
|
523
|
+
State(..s, lives: 0, over: true, overAt: s.clock, best: s.best.max(s.score))
|
|
614
|
-
n
|
|
615
524
|
|
|
616
525
|
fun countNodes(n: SceneNode) -> Int = match n
|
|
617
526
|
Group(_, kids) => 1 + countKids(kids)
|
|
@@ -623,26 +532,26 @@ fun countKids(ks: List[SceneNode]) -> Int = match ks
|
|
|
623
532
|
Cons(h, t) => countNodes(h) + countKids(t)
|
|
624
533
|
|
|
625
534
|
test "steering accelerates and drag slows the ship"
|
|
626
|
-
s :=
|
|
535
|
+
s := Ship.start().update(1.0, 0.0, 0.1)
|
|
627
536
|
assert s.vx > 0.0
|
|
628
537
|
assert s.x > Rules.ArenaW / 2.0
|
|
629
538
|
fast := s.vx
|
|
630
539
|
t := s
|
|
631
540
|
for i := range 30
|
|
632
|
-
t =
|
|
541
|
+
t = t.update(0.0, 0.0, 0.1)
|
|
633
542
|
assert t.vx < fast / 4.0
|
|
634
543
|
|
|
635
544
|
test "the ship stops at the arena walls"
|
|
636
|
-
s :=
|
|
545
|
+
s := Ship(x: 20.0, y: 20.0, vx: 0.0, vy: 0.0, rotation: 0.0, hurt: 0.0)
|
|
637
546
|
for i := range 20
|
|
638
|
-
s =
|
|
547
|
+
s = s.update(-1.0, -1.0, 0.1)
|
|
639
548
|
assert s.x == Rules.ShipRadius
|
|
640
549
|
assert s.y == Rules.ShipRadius
|
|
641
550
|
assert s.vx == 0.0
|
|
642
551
|
|
|
643
552
|
test "shipStep leaves its input unchanged"
|
|
644
|
-
s :=
|
|
553
|
+
s := Ship.start()
|
|
645
|
-
_ :=
|
|
554
|
+
_ := s.update(1.0, 1.0, 0.1)
|
|
646
555
|
assert s.vx == 0.0
|
|
647
556
|
assert s.x == Rules.ArenaW / 2.0
|
|
648
557
|
|
|
@@ -650,24 +559,24 @@ test "drifters bounce off the walls"
|
|
|
650
559
|
b := driftOf(Motion(x: 795.0, y: 300.0, vx: 100.0, vy: 0.0), Rules.OrbRadius, 0.1)
|
|
651
560
|
assert b.vx == -100.0
|
|
652
561
|
assert b.x == Rules.ArenaW - 10.0
|
|
653
|
-
orb :=
|
|
562
|
+
orb := Orb(id: Id.Orb, at: Motion(x: 795.0, y: 300.0, vx: 100.0, vy: 0.0), phase: 0.0, clock: 0.0).update(0.1)
|
|
654
|
-
assert near(orb.x, Rules.ArenaW - 10.0)
|
|
563
|
+
assert near(orb.at.x, Rules.ArenaW - 10.0)
|
|
655
564
|
|
|
656
565
|
test "a mine spins as it drifts"
|
|
657
|
-
mine :=
|
|
566
|
+
mine := Mine(id: Id.Mine, at: Motion(x: 400.0, y: 300.0, vx: 10.0, vy: 0.0), rotation: 0.0, spin: 2.0, clock: 0.0)
|
|
658
|
-
next :=
|
|
567
|
+
next := mine.update(0.5)
|
|
659
568
|
assert near(next.rotation, 1.0)
|
|
660
|
-
assert near(next.x, 405.0)
|
|
569
|
+
assert near(next.at.x, 405.0)
|
|
661
570
|
assert near(mine.rotation, 0.0)
|
|
662
571
|
|
|
663
572
|
test "collecting an orb scores and respawns it elsewhere"
|
|
664
573
|
s := withOrb(emptyState(), 0, onShip(emptyState()))
|
|
665
574
|
t := tickOf(s, 0.0, 0.0, 0.016)
|
|
666
|
-
orb :=
|
|
575
|
+
orb := t.orbs.at(0)
|
|
667
576
|
assert t.score == Rules.OrbPoints
|
|
668
|
-
assert Math.hypot(orb.x - t.ship.x, orb.y - t.ship.y) >= 120.0
|
|
577
|
+
assert Math.hypot(orb.at.x - t.ship.x, orb.at.y - t.ship.y) >= 120.0
|
|
669
|
-
assert orb.x < 5000.0
|
|
578
|
+
assert orb.at.x < 5000.0
|
|
670
|
-
assert
|
|
579
|
+
assert t.orbs.length() == Rules.OrbCount
|
|
671
580
|
|
|
672
581
|
test "a mine costs one life and makes the ship flash"
|
|
673
582
|
e := emptyState()
|
|
@@ -680,14 +589,14 @@ test "a mine costs one life and makes the ship flash"
|
|
|
680
589
|
shown := 0
|
|
681
590
|
for i := range 100
|
|
682
591
|
t = tickOf(t, 0.0, 0.0, 0.016)
|
|
683
|
-
if countNodes(
|
|
592
|
+
if countNodes(t.renderShip()) == 1
|
|
684
593
|
hidden = hidden + 1
|
|
685
594
|
else
|
|
686
595
|
shown = shown + 1
|
|
687
596
|
assert hidden > 0
|
|
688
597
|
assert shown > 0
|
|
689
598
|
assert t.ship.hurt == 0.0
|
|
690
|
-
assert countNodes(
|
|
599
|
+
assert countNodes(t.renderShip()) == 3
|
|
691
600
|
assert t.lives == Rules.StartLives - 1
|
|
692
601
|
|
|
693
602
|
test "the ship is briefly safe after a hit"
|
|
@@ -698,80 +607,74 @@ test "the ship is briefly safe after a hit"
|
|
|
698
607
|
|
|
699
608
|
test "the game ends at zero lives and then ignores steps"
|
|
700
609
|
e := emptyState()
|
|
701
|
-
s := State(..e)
|
|
610
|
+
s := State(..e, lives: 1, score: 40)
|
|
702
|
-
s.lives = 1
|
|
703
|
-
s.score = 40
|
|
704
611
|
t := tickOf(withMine(s, 0, onShip(s)), 0.0, 0.0, 0.016)
|
|
705
612
|
assert t.over
|
|
706
613
|
assert t.lives == 0
|
|
707
614
|
assert t.best == 40
|
|
708
|
-
assert countNodes(
|
|
615
|
+
assert countNodes(t.renderShip()) == 1
|
|
709
616
|
x := t.ship.x
|
|
710
617
|
u := tickOf(t, 1.0, 0.0, 0.5)
|
|
711
618
|
assert u.ship.x == x
|
|
712
619
|
|
|
713
620
|
test "after game over nothing moves but the animations keep running"
|
|
714
621
|
e := emptyState()
|
|
715
|
-
s := State(..e)
|
|
622
|
+
s := State(..e, lives: 1)
|
|
716
|
-
s.lives = 1
|
|
717
623
|
t := tickOf(withMine(s, 0, Motion(x: s.ship.x, y: s.ship.y, vx: 10.0, vy: 0.0)), 0.0, 0.0, 0.016)
|
|
718
624
|
assert t.over
|
|
719
|
-
mine :=
|
|
625
|
+
mine := t.mines.at(0)
|
|
720
626
|
for i := range 10
|
|
721
627
|
t = tickOf(t, 1.0, 0.0, 0.1)
|
|
722
|
-
after :=
|
|
628
|
+
after := t.mines.at(0)
|
|
723
|
-
assert after.x == mine.x
|
|
629
|
+
assert after.at.x == mine.at.x
|
|
724
630
|
assert after.rotation == mine.rotation
|
|
725
631
|
assert t.ship.x == Rules.ArenaW / 2.0
|
|
726
632
|
assert after.clock > mine.clock + 0.9
|
|
727
|
-
assert
|
|
633
|
+
assert t.orbs.at(0).clock > 0.9
|
|
728
634
|
assert t.clock > 0.9
|
|
729
635
|
assert t.score == 0
|
|
730
636
|
|
|
731
637
|
test "restart resets the run and keeps the best score"
|
|
732
638
|
e := emptyState()
|
|
733
|
-
s := State(..e)
|
|
734
|
-
s.score = 90
|
|
735
|
-
s = forceOver(
|
|
639
|
+
s := forceOver(State(..e, score: 90))
|
|
736
|
-
r :=
|
|
640
|
+
r := s.update(Restart(seed: 5))
|
|
737
641
|
assert !r.over
|
|
738
642
|
assert r.score == 0
|
|
739
643
|
assert r.best == 90
|
|
740
644
|
assert r.lives == Rules.StartLives
|
|
741
645
|
assert r.ship.x == Rules.ArenaW / 2.0
|
|
742
|
-
assert
|
|
646
|
+
assert r.mines.length() == mineCount(0)
|
|
743
|
-
assert countNodes(
|
|
647
|
+
assert countNodes(r.renderShip()) == 3
|
|
744
|
-
assert countNodes(
|
|
648
|
+
assert countNodes(r.renderOverlay()) == 1
|
|
745
649
|
|
|
746
650
|
test "more mines appear as the score grows and restart removes them"
|
|
747
651
|
assert mineCount(0) == 3
|
|
748
652
|
assert mineCount(100) == 5
|
|
749
653
|
assert mineCount(10000) == 8
|
|
750
654
|
e := emptyState()
|
|
751
|
-
s := State(..e)
|
|
655
|
+
s := State(..e, score: 100)
|
|
752
|
-
s.score = 100
|
|
753
656
|
t := tickOf(s, 0.0, 0.0, 0.016)
|
|
754
|
-
assert
|
|
657
|
+
assert t.mines.length() == 5
|
|
755
|
-
r :=
|
|
658
|
+
r := t.update(Restart(seed: 3))
|
|
756
|
-
assert
|
|
659
|
+
assert r.mines.length() == 3
|
|
757
660
|
|
|
758
661
|
test "the same seed plays out the same way"
|
|
759
|
-
a := newState(11
|
|
662
|
+
a := newState(11)
|
|
760
|
-
b := newState(11
|
|
663
|
+
b := newState(11)
|
|
761
664
|
for i := range 120
|
|
762
665
|
a = tickOf(a, 1.0, -1.0, 0.016)
|
|
763
666
|
b = tickOf(b, 1.0, -1.0, 0.016)
|
|
764
667
|
assert a.ship.x == b.ship.x
|
|
765
668
|
assert a.score == b.score
|
|
766
|
-
assert
|
|
669
|
+
assert a.mines.at(0).at.x == b.mines.at(0).at.x
|
|
767
|
-
other := newState(12
|
|
670
|
+
other := newState(12)
|
|
768
|
-
assert
|
|
671
|
+
assert other.orbs.at(0).at.x != a.orbs.at(0).at.x
|
|
769
672
|
|
|
770
673
|
test "fit centres the arena on the screen"
|
|
771
|
-
s :=
|
|
674
|
+
s := emptyState().update(Resize(w: 1600.0, h: 600.0))
|
|
772
|
-
assert near(
|
|
675
|
+
assert near(s.zoom(), 1.0)
|
|
773
|
-
assert near(offsetX(
|
|
676
|
+
assert near(s.offsetX(), 400.0)
|
|
774
|
-
assert near(offsetY(
|
|
677
|
+
assert near(s.offsetY(), 0.0)
|
|
775
678
|
|
|
776
679
|
test "animation frames cycle"
|
|
777
680
|
assert frameOf(0.0, 4, 8.0) == 0
|
|
@@ -782,21 +685,21 @@ test "animation frames cycle"
|
|
|
782
685
|
|
|
783
686
|
test "restart_button_hidden_while_playing"
|
|
784
687
|
s := emptyState()
|
|
785
|
-
assert !onRestart(
|
|
688
|
+
assert !s.onRestart(400.0, 364.0)
|
|
786
689
|
|
|
787
690
|
test "restart_button_click_restarts"
|
|
788
691
|
s := forceOver(emptyState())
|
|
789
|
-
assert onRestart(
|
|
692
|
+
assert s.onRestart(400.0, 364.0)
|
|
790
|
-
assert !onRestart(
|
|
693
|
+
assert !s.onRestart(100.0, 100.0)
|
|
791
|
-
assert !onRestart(
|
|
694
|
+
assert !s.onRestart(400.0, 260.0)
|
|
792
|
-
r :=
|
|
695
|
+
r := s.update(Restart(seed: 9))
|
|
793
696
|
assert !r.over
|
|
794
|
-
assert !onRestart(
|
|
697
|
+
assert !r.onRestart(400.0, 364.0)
|
|
795
698
|
|
|
796
699
|
test "the restart button is found through the camera"
|
|
797
|
-
s :=
|
|
700
|
+
s := forceOver(emptyState()).update(Resize(w: 1600.0, h: 600.0))
|
|
798
|
-
assert onRestart(
|
|
701
|
+
assert s.onRestart(800.0, 364.0)
|
|
799
|
-
assert !onRestart(
|
|
702
|
+
assert !s.onRestart(400.0, 364.0)
|
|
800
703
|
|
|
801
704
|
test "update_is_pure"
|
|
802
705
|
s := emptyState()
|
|
@@ -809,49 +712,47 @@ test "freezing at game over keeps the orb and mine animation clocks"
|
|
|
809
712
|
t := emptyState()
|
|
810
713
|
for i := range 5
|
|
811
714
|
t = tickOf(t, 0.0, 0.0, 0.1)
|
|
812
|
-
orbClock :=
|
|
715
|
+
orbClock := t.orbs.at(0).clock
|
|
813
|
-
mineClock :=
|
|
716
|
+
mineClock := t.mines.at(0).clock
|
|
814
717
|
assert orbClock > 0.4
|
|
815
|
-
t
|
|
718
|
+
t = State(..t, lives: 1)
|
|
816
719
|
t = tickOf(withMine(t, 1, onShip(t)), 0.0, 0.0, 0.1)
|
|
817
720
|
assert t.over
|
|
818
|
-
assert
|
|
721
|
+
assert t.orbs.at(0).clock > orbClock
|
|
819
|
-
assert
|
|
722
|
+
assert t.mines.at(0).clock > mineClock
|
|
820
723
|
|
|
821
724
|
test "view_is_pure"
|
|
822
725
|
s := emptyState()
|
|
823
|
-
a := countNodes(
|
|
726
|
+
a := countNodes(s.render())
|
|
824
|
-
b := countNodes(
|
|
727
|
+
b := countNodes(s.render())
|
|
825
728
|
assert a == b
|
|
826
|
-
ba := Picture.bounds(
|
|
729
|
+
ba := Picture.bounds(s.render())
|
|
827
|
-
bb := Picture.bounds(
|
|
730
|
+
bb := Picture.bounds(s.render())
|
|
828
731
|
assert ba.x == bb.x
|
|
829
732
|
assert ba.w == bb.w
|
|
830
733
|
assert s.clock == 0.0
|
|
831
734
|
|
|
832
735
|
test "view_node_count"
|
|
833
|
-
s := newState(7
|
|
736
|
+
s := newState(7)
|
|
834
|
-
assert countNodes(
|
|
737
|
+
assert countNodes(s.render()) == 79
|
|
835
738
|
over := forceOver(s)
|
|
836
|
-
assert countNodes(
|
|
739
|
+
assert countNodes(over.renderOverlay()) > 5
|
|
837
|
-
assert countNodes(
|
|
740
|
+
assert countNodes(over.render()) > countNodes(s.render()) - 3
|
|
838
741
|
|
|
839
742
|
test "restart_click_through_ui"
|
|
840
743
|
e := emptyState()
|
|
841
|
-
s := State(..e)
|
|
842
|
-
s.score = 90
|
|
843
|
-
s = forceOver(
|
|
744
|
+
s := forceOver(State(..e, score: 90))
|
|
844
|
-
down := feed(
|
|
745
|
+
down := s.feed(UiInput(kind: PointerDown, x: 400.0, y: 364.0))
|
|
845
746
|
assert down.over
|
|
846
747
|
assert Ui.pressed(down.ui, Id.Restart)
|
|
847
|
-
up := feed(
|
|
748
|
+
up := down.feed(UiInput(kind: PointerUp, x: 400.0, y: 364.0))
|
|
848
749
|
assert !up.over
|
|
849
750
|
assert up.score == 0
|
|
850
751
|
assert up.best == 90
|
|
851
752
|
assert up.lives == Rules.StartLives
|
|
852
|
-
off := feed(
|
|
753
|
+
off := s.feed(UiInput(kind: PointerDown, x: 400.0, y: 364.0)).feed(UiInput(kind: PointerUp, x: 100.0, y: 100.0))
|
|
853
754
|
assert off.over
|
|
854
|
-
miss := feed(
|
|
755
|
+
miss := s.feed(UiInput(kind: PointerDown, x: 100.0, y: 100.0)).feed(UiInput(kind: PointerUp, x: 100.0, y: 100.0))
|
|
855
756
|
assert miss.over
|
|
856
757
|
|
|
857
758
|
test "hit_flash_pose_at_fixed_times"
|
plum-examples/games/imperative/game.plum
CHANGED
|
@@ -34,19 +34,19 @@ primitive Rules =
|
|
|
34
34
|
Zoom = 3.0
|
|
35
35
|
|
|
36
36
|
# A moving circle. `angle` is the ship's heading or a mine's spin, in radians clockwise.
|
|
37
|
-
record Body(x: Float, y: Float, vx: Float, vy: Float, angle: Float)
|
|
37
|
+
record Body(mut x: Float, mut y: Float, mut vx: Float, mut vy: Float, mut angle: Float)
|
|
38
38
|
|
|
39
39
|
record World(
|
|
40
40
|
ship: Body,
|
|
41
41
|
orbs: Slice[Body],
|
|
42
42
|
mines: Slice[Body],
|
|
43
|
-
rng: Random,
|
|
43
|
+
mut rng: Random,
|
|
44
|
-
score: Int,
|
|
44
|
+
mut score: Int,
|
|
45
|
-
best: Int,
|
|
45
|
+
mut best: Int,
|
|
46
|
-
lives: Int,
|
|
46
|
+
mut lives: Int,
|
|
47
|
-
hurt: Float,
|
|
47
|
+
mut hurt: Float,
|
|
48
|
-
clock: Float,
|
|
48
|
+
mut clock: Float,
|
|
49
|
-
over: Bool,
|
|
49
|
+
mut over: Bool,
|
|
50
50
|
sheet: Int,
|
|
51
51
|
pickupSound: Int,
|
|
52
52
|
hitSound: Int,
|
|
@@ -54,16 +54,19 @@ record World(
|
|
|
54
54
|
musicSound: Int) =
|
|
55
55
|
|
|
56
56
|
fun create(seed: Int, sheet: Int) -> World =
|
|
57
|
-
rng := Random(seed: seed)
|
|
58
57
|
ship := Body(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, angle: 0.0)
|
|
59
58
|
w := World(
|
|
60
|
-
ship: ship, orbs: Slice[Body](), mines: Slice[Body](), rng:
|
|
59
|
+
ship: ship, orbs: Slice[Body](), mines: Slice[Body](), rng: Random.seeded(seed), score: 0, best: 0,
|
|
61
60
|
lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, sheet: sheet,
|
|
62
61
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
63
62
|
for i := range Rules.OrbCount
|
|
64
|
-
|
|
63
|
+
sp := spawn(w.rng, ship, 120.0, 40.0)
|
|
64
|
+
w.rng = sp.rng
|
|
65
|
+
w.orbs.push(sp.at)
|
|
65
66
|
for i := range mineCount(0)
|
|
66
|
-
|
|
67
|
+
sp := spawn(w.rng, ship, 220.0, 90.0)
|
|
68
|
+
w.rng = sp.rng
|
|
69
|
+
w.mines.push(sp.at)
|
|
67
70
|
w
|
|
68
71
|
|
|
69
72
|
# A world with the orbs and mines moved out of the way.
|
|
@@ -91,7 +94,9 @@ record World(
|
|
|
91
94
|
self.ship.vx = 0.0
|
|
92
95
|
self.ship.vy = 0.0
|
|
93
96
|
for i := range self.orbs.length()
|
|
94
|
-
|
|
97
|
+
sp := spawn(self.rng, self.ship, 120.0, 40.0)
|
|
98
|
+
self.rng = sp.rng
|
|
99
|
+
moved := sp.at
|
|
95
100
|
self.orbs.at(i).x = moved.x
|
|
96
101
|
self.orbs.at(i).y = moved.y
|
|
97
102
|
self.orbs.at(i).vx = moved.vx
|
|
@@ -99,7 +104,9 @@ record World(
|
|
|
99
104
|
while self.mines.length() > mineCount(0)
|
|
100
105
|
_ := self.mines.pop()!
|
|
101
106
|
for i := range self.mines.length()
|
|
102
|
-
|
|
107
|
+
sp := spawn(self.rng, self.ship, 220.0, 90.0)
|
|
108
|
+
self.rng = sp.rng
|
|
109
|
+
moved := sp.at
|
|
103
110
|
self.mines.at(i).x = moved.x
|
|
104
111
|
self.mines.at(i).y = moved.y
|
|
105
112
|
self.mines.at(i).vx = moved.vx
|
|
@@ -117,13 +124,17 @@ record World(
|
|
|
117
124
|
drift(orb, Rules.OrbRadius, dt)
|
|
118
125
|
if overlaps(self.ship, Rules.ShipRadius, orb, Rules.OrbRadius)
|
|
119
126
|
self.score = self.score + Rules.OrbPoints
|
|
120
|
-
|
|
127
|
+
sp := spawn(self.rng, self.ship, 120.0, 40.0)
|
|
128
|
+
self.rng = sp.rng
|
|
129
|
+
moved := sp.at
|
|
121
130
|
orb.x = moved.x
|
|
122
131
|
orb.y = moved.y
|
|
123
132
|
orb.vx = moved.vx
|
|
124
133
|
orb.vy = moved.vy
|
|
125
134
|
while self.mines.length() < mineCount(self.score)
|
|
126
|
-
|
|
135
|
+
sp := spawn(self.rng, self.ship, 220.0, 90.0)
|
|
136
|
+
self.rng = sp.rng
|
|
137
|
+
self.mines.push(sp.at)
|
|
127
138
|
for i := range self.mines.length()
|
|
128
139
|
mine := self.mines.at(i)
|
|
129
140
|
drift(mine, Rules.MineRadius, dt)
|
|
@@ -220,21 +231,30 @@ fun frameOf(clock: Float, count: Int, fps: Float) -> Int =
|
|
|
220
231
|
Int(clock * fps) % count
|
|
221
232
|
|
|
222
233
|
# A body at a random spot at least `gap` away from `avoid`, drifting at up to `speed`.
|
|
234
|
+
# A Body and the generator to continue from.
|
|
235
|
+
record Spawned(at: Body, rng: Random)
|
|
236
|
+
|
|
223
|
-
fun spawn(rng: Random, avoid: Body, gap: Float, speed: Float) ->
|
|
237
|
+
fun spawn(rng: Random, avoid: Body, gap: Float, speed: Float) -> Spawned =
|
|
224
238
|
margin := 30.0
|
|
225
239
|
b := Body(x: 0.0, y: 0.0, vx: 0.0, vy: 0.0, angle: 0.0)
|
|
240
|
+
r := rng
|
|
226
241
|
tries := 0
|
|
227
242
|
while tries < 20
|
|
243
|
+
dx := r.nextFloat()
|
|
244
|
+
dy := dx.rng.nextFloat()
|
|
245
|
+
r = dy.rng
|
|
228
|
-
b.x = margin +
|
|
246
|
+
b.x = margin + dx.value * {Rules.ArenaW - 2.0 * margin}
|
|
229
|
-
b.y = margin +
|
|
247
|
+
b.y = margin + dy.value * {Rules.ArenaH - 2.0 * margin}
|
|
230
248
|
if distance(b, avoid) >= gap
|
|
231
249
|
break
|
|
232
250
|
tries = tries + 1
|
|
251
|
+
dh := r.nextFloat()
|
|
252
|
+
dp := dh.rng.nextFloat()
|
|
233
|
-
heading :=
|
|
253
|
+
heading := dh.value * Float.Tau
|
|
234
|
-
pace := speed * {0.4 + 0.6 *
|
|
254
|
+
pace := speed * {0.4 + 0.6 * dp.value}
|
|
235
255
|
b.vx = Math.cos(heading) * pace
|
|
236
256
|
b.vy = Math.sin(heading) * pace
|
|
237
|
-
b
|
|
257
|
+
Spawned(at: b, rng: dp.rng)
|
|
238
258
|
|
|
239
259
|
# How many mines are out: more as the score grows.
|
|
240
260
|
fun mineCount(score: Int) -> Int =
|
|
@@ -282,7 +302,7 @@ fun musicVolume() -> Float =
|
|
|
282
302
|
0.3
|
|
283
303
|
|
|
284
304
|
export fun init(io: Io) -> World =
|
|
285
|
-
w := World.create(
|
|
305
|
+
w := World.create(io.randomSeed(), io.textureLoad("sprites.png"))
|
|
286
306
|
w.pickupSound = io.soundLoad("pickup.wav")
|
|
287
307
|
w.hitSound = io.soundLoad("hit.wav")
|
|
288
308
|
w.overSound = io.soundLoad("gameover.wav")
|
plum-examples/{game_loop.plum → games/imperative/game_loop.plum}
RENAMED
|
@@ -19,7 +19,7 @@ struct Vec2(x: Float, y: Float) =
|
|
|
19
19
|
|
|
20
20
|
struct Particle(pos: Vec2, vel: Vec2, life: Int)
|
|
21
21
|
|
|
22
|
-
record Entity(id: Int, name: Str, hp: Int)
|
|
22
|
+
record Entity(id: Int, name: Str, mut hp: Int)
|
|
23
23
|
|
|
24
24
|
record Spawn(id: Int, name: Str)
|
|
25
25
|
|
plum-examples/games/imperative/game_scene.plum
CHANGED
|
@@ -49,28 +49,37 @@ primitive Rules =
|
|
|
49
49
|
Zoom = 3.0
|
|
50
50
|
|
|
51
51
|
# A position and velocity: what a spawn returns and what drifters are bounced with.
|
|
52
|
-
record Motion(x: Float, y: Float, vx: Float, vy: Float)
|
|
52
|
+
record Motion(mut x: Float, mut y: Float, mut vx: Float, mut vy: Float)
|
|
53
53
|
|
|
54
54
|
# Which of `count` animation frames is showing after `clock` seconds at `fps` frames per second.
|
|
55
55
|
fun frameOf(clock: Float, count: Int, fps: Float) -> Int =
|
|
56
56
|
Int(clock * fps) % count
|
|
57
57
|
|
|
58
58
|
# A random spot at least `gap` away from (ax, ay), drifting at up to `speed`.
|
|
59
|
+
# A Motion and the generator to continue from.
|
|
60
|
+
record Spawned(at: Motion, rng: Random)
|
|
61
|
+
|
|
59
|
-
fun spawn(rng: Random, ax: Float, ay: Float, gap: Float, speed: Float) ->
|
|
62
|
+
fun spawn(rng: Random, ax: Float, ay: Float, gap: Float, speed: Float) -> Spawned =
|
|
60
63
|
margin := 30.0
|
|
61
64
|
b := Motion(x: 0.0, y: 0.0, vx: 0.0, vy: 0.0)
|
|
65
|
+
r := rng
|
|
62
66
|
tries := 0
|
|
63
67
|
while tries < 20
|
|
68
|
+
dx := r.nextFloat()
|
|
69
|
+
dy := dx.rng.nextFloat()
|
|
70
|
+
r = dy.rng
|
|
64
|
-
b.x = margin +
|
|
71
|
+
b.x = margin + dx.value * {Rules.ArenaW - 2.0 * margin}
|
|
65
|
-
b.y = margin +
|
|
72
|
+
b.y = margin + dy.value * {Rules.ArenaH - 2.0 * margin}
|
|
66
73
|
if Math.hypot(b.x - ax, b.y - ay) >= gap
|
|
67
74
|
break
|
|
68
75
|
tries = tries + 1
|
|
76
|
+
dh := r.nextFloat()
|
|
77
|
+
dp := dh.rng.nextFloat()
|
|
69
|
-
heading :=
|
|
78
|
+
heading := dh.value * Float.Tau
|
|
70
|
-
pace := speed * {0.4 + 0.6 *
|
|
79
|
+
pace := speed * {0.4 + 0.6 * dp.value}
|
|
71
80
|
b.vx = Math.cos(heading) * pace
|
|
72
81
|
b.vy = Math.sin(heading) * pace
|
|
73
|
-
b
|
|
82
|
+
Spawned(at: b, rng: dp.rng)
|
|
74
83
|
|
|
75
84
|
# Moves a drifter and bounces it off the arena walls.
|
|
76
85
|
fun drift(b: Motion, r: Float, dt: Float) =
|
|
@@ -91,20 +100,13 @@ fun mineCount(score: Int) -> Int =
|
|
|
91
100
|
# (origin at the middle of the box, so `rotation` turns it about its centre).
|
|
92
101
|
|
|
93
102
|
# The player. `ax` and `ay` are the input axes (-1..1) for the next `act`.
|
|
94
|
-
record Ship(Actor(x, y), tex: Int) =
|
|
103
|
+
record Ship(Actor(x, y), tex: Int, mut vx: Float, mut vy: Float, mut ax: Float, mut ay: Float, mut hurt: Float, mut clock: Float, mut live: Bool) =
|
|
95
104
|
|
|
96
105
|
fun init(self) =
|
|
97
106
|
self.width = Rules.Cell * Rules.Zoom
|
|
98
107
|
self.height = self.width
|
|
99
108
|
self.originX = self.width / 2.0
|
|
100
109
|
self.originY = self.height / 2.0
|
|
101
|
-
self.vx = 0.0
|
|
102
|
-
self.vy = 0.0
|
|
103
|
-
self.ax = 0.0
|
|
104
|
-
self.ay = 0.0
|
|
105
|
-
self.hurt = 0.0
|
|
106
|
-
self.clock = 0.0
|
|
107
|
-
self.live = true
|
|
108
110
|
|
|
109
111
|
# Accelerates by the input axes, applies drag, keeps the ship in the arena and
|
|
110
112
|
# blinks it while it is hurt (the blink is just `visible`).
|
|
@@ -144,18 +146,16 @@ record Ship(Actor(x, y), tex: Int) =
|
|
|
144
146
|
drawSpriteFrame(io, m, alpha, self.width, self.height, self.color, self.tex, frame * Rules.Cell, 0.0, Rules.Cell, Rules.Cell)
|
|
145
147
|
|
|
146
148
|
# A glowing orb drifting about. `phase` offsets its animation from the other orbs.
|
|
147
|
-
record Orb(Actor(x, y), vx: Float, vy: Float, phase: Float, tex: Int) =
|
|
149
|
+
record Orb(Actor(x, y), mut vx: Float, mut vy: Float, phase: Float, tex: Int, mut clock: Float, mut live: Bool) =
|
|
148
150
|
|
|
149
151
|
fun init(self) =
|
|
150
152
|
self.width = Rules.Cell * Rules.Zoom
|
|
151
153
|
self.height = self.width
|
|
152
154
|
self.originX = self.width / 2.0
|
|
153
155
|
self.originY = self.height / 2.0
|
|
154
|
-
self.clock = 0.0
|
|
155
|
-
self.live = true
|
|
156
156
|
|
|
157
157
|
fun create(sheet: Int, phase: Float, at: Motion) -> Orb =
|
|
158
|
-
Orb(x: at.x, y: at.y, vx: at.vx, vy: at.vy, phase: phase, tex: sheet)
|
|
158
|
+
Orb(x: at.x, y: at.y, vx: at.vx, vy: at.vy, phase: phase, tex: sheet, clock: 0.0, live: true)
|
|
159
159
|
|
|
160
160
|
fun motion(self) -> Motion =
|
|
161
161
|
Motion(x: self.x, y: self.y, vx: self.vx, vy: self.vy)
|
|
@@ -185,18 +185,16 @@ record Orb(Actor(x, y), vx: Float, vy: Float, phase: Float, tex: Int) =
|
|
|
185
185
|
drawSpriteFrame(io, m, alpha, self.width, self.height, self.color, self.tex, frame * Rules.Cell, Rules.Cell, Rules.Cell, Rules.Cell)
|
|
186
186
|
|
|
187
187
|
# A spinning mine drifting about.
|
|
188
|
-
record Mine(Actor(x, y), vx: Float, vy: Float, tex: Int) =
|
|
188
|
+
record Mine(Actor(x, y), mut vx: Float, mut vy: Float, tex: Int, mut clock: Float, mut live: Bool) =
|
|
189
189
|
|
|
190
190
|
fun init(self) =
|
|
191
191
|
self.width = Rules.Cell * Rules.Zoom
|
|
192
192
|
self.height = self.width
|
|
193
193
|
self.originX = self.width / 2.0
|
|
194
194
|
self.originY = self.height / 2.0
|
|
195
|
-
self.clock = 0.0
|
|
196
|
-
self.live = true
|
|
197
195
|
|
|
198
196
|
fun create(sheet: Int, at: Motion) -> Mine =
|
|
199
|
-
Mine(x: at.x, y: at.y, vx: at.vx, vy: at.vy, tex: sheet)
|
|
197
|
+
Mine(x: at.x, y: at.y, vx: at.vx, vy: at.vy, tex: sheet, clock: 0.0, live: true)
|
|
200
198
|
|
|
201
199
|
fun motion(self) -> Motion =
|
|
202
200
|
Motion(x: self.x, y: self.y, vx: self.vx, vy: self.vy)
|
|
@@ -221,16 +219,13 @@ record Mine(Actor(x, y), vx: Float, vy: Float, tex: Int) =
|
|
|
221
219
|
drawSpriteFrame(io, m, alpha, self.width, self.height, self.color, self.tex, frame * Rules.Cell, 2.0 * Rules.Cell, Rules.Cell, Rules.Cell)
|
|
222
220
|
|
|
223
221
|
# A background star that twinkles.
|
|
224
|
-
record Star(Actor(x, y, width, height), seed: Float) =
|
|
222
|
+
record Star(Actor(x, y, width, height), seed: Float, mut clock: Float) =
|
|
225
|
-
|
|
226
|
-
fun init(self) =
|
|
227
|
-
self.clock = 0.0
|
|
228
223
|
|
|
229
224
|
fun create(i: Int) -> Star =
|
|
230
225
|
r := 1.0 + Float(i % 3) * 0.5
|
|
231
226
|
Star(
|
|
232
227
|
x: Float({i * 7919} % 800) - r, y: Float({i * 104729} % 600) - r, width: r * 2.0,
|
|
233
|
-
height: r * 2.0, seed: Float(i))
|
|
228
|
+
height: r * 2.0, seed: Float(i), clock: 0.0)
|
|
234
229
|
|
|
235
230
|
override fun act(self, dt: Float) =
|
|
236
231
|
self.clock = self.clock + dt
|
|
@@ -243,7 +238,7 @@ record Star(Actor(x, y, width, height), seed: Float) =
|
|
|
243
238
|
# touch, and the run's state. `mines` and `orbs` are the layer Groups; `mineList`
|
|
244
239
|
# and `orbList` are the same actors, typed.
|
|
245
240
|
record World(
|
|
246
|
-
Stage(camScale),
|
|
241
|
+
Stage(camX, camY, camScale),
|
|
247
242
|
ship: Ship,
|
|
248
243
|
mines: Group,
|
|
249
244
|
orbs: Group,
|
|
@@ -253,12 +248,12 @@ record World(
|
|
|
253
248
|
scoreText: Text,
|
|
254
249
|
bestText: Text,
|
|
255
250
|
lifeIcons: Slice[Sprite],
|
|
256
|
-
rng: Random,
|
|
251
|
+
mut rng: Random,
|
|
257
|
-
score: Int,
|
|
252
|
+
mut score: Int,
|
|
258
|
-
best: Int,
|
|
253
|
+
mut best: Int,
|
|
259
|
-
lives: Int,
|
|
254
|
+
mut lives: Int,
|
|
260
|
-
over: Bool,
|
|
255
|
+
mut over: Bool,
|
|
261
|
-
clock: Float,
|
|
256
|
+
mut clock: Float,
|
|
262
257
|
sheet: Int,
|
|
263
258
|
pickupSound: Int,
|
|
264
259
|
hitSound: Int,
|
|
@@ -266,8 +261,7 @@ record World(
|
|
|
266
261
|
musicSound: Int) =
|
|
267
262
|
|
|
268
263
|
fun create(seed: Int, sheet: Int) -> World =
|
|
269
|
-
rng := Random(seed: seed)
|
|
270
|
-
ship := Ship(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, tex: sheet)
|
|
264
|
+
ship := Ship(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, tex: sheet, vx: 0.0, vy: 0.0, ax: 0.0, ay: 0.0, hurt: 0.0, clock: 0.0, live: true)
|
|
271
265
|
stars := Group()
|
|
272
266
|
for i := range 40
|
|
273
267
|
stars.add(Star.create(i))
|
|
@@ -296,9 +290,9 @@ record World(
|
|
|
296
290
|
overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
|
|
297
291
|
hud.add(overlay)
|
|
298
292
|
w := World(
|
|
299
|
-
camScale: 1.0, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
|
|
293
|
+
camX: 0.0, camY: 0.0, camScale: 1.0, ship: ship, mines: mines, orbs: orbs, mineList: Slice[Mine](), orbList: Slice[Orb](),
|
|
300
294
|
overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
|
|
301
|
-
rng:
|
|
295
|
+
rng: Random.seeded(seed), score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, sheet: sheet,
|
|
302
296
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
303
297
|
w.root.add(Rect.create(Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)))
|
|
304
298
|
w.root.add(stars)
|
|
@@ -307,7 +301,9 @@ record World(
|
|
|
307
301
|
w.root.add(ship)
|
|
308
302
|
w.root.add(hud)
|
|
309
303
|
for i := range Rules.OrbCount
|
|
304
|
+
sp := spawn(w.rng, ship.x, ship.y, 120.0, 40.0)
|
|
305
|
+
w.rng = sp.rng
|
|
310
|
-
orb := Orb.create(sheet, Float(i) * 0.1,
|
|
306
|
+
orb := Orb.create(sheet, Float(i) * 0.1, sp.at)
|
|
311
307
|
orbs.add(orb)
|
|
312
308
|
w.orbList.push(orb)
|
|
313
309
|
for i := range mineCount(0)
|
|
@@ -325,7 +321,9 @@ record World(
|
|
|
325
321
|
|
|
326
322
|
# Adds one mine, far from the ship, to the mines layer.
|
|
327
323
|
fun addMine(self) =
|
|
328
|
-
|
|
324
|
+
sp := spawn(self.rng, self.ship.x, self.ship.y, 220.0, 90.0)
|
|
325
|
+
self.rng = sp.rng
|
|
326
|
+
mine := Mine.create(self.sheet, sp.at)
|
|
329
327
|
self.mines.add(mine)
|
|
330
328
|
self.mineList.push(mine)
|
|
331
329
|
|
|
@@ -343,12 +341,16 @@ record World(
|
|
|
343
341
|
ship.vx = 0.0
|
|
344
342
|
ship.vy = 0.0
|
|
345
343
|
for i := range self.orbList.length()
|
|
346
|
-
|
|
344
|
+
sp := spawn(self.rng, ship.x, ship.y, 120.0, 40.0)
|
|
345
|
+
self.rng = sp.rng
|
|
346
|
+
self.orbList.at(i).place(sp.at)
|
|
347
347
|
while self.mineList.length() > mineCount(0)
|
|
348
348
|
gone := self.mineList.pop()!
|
|
349
349
|
self.mines.remove(gone)
|
|
350
350
|
for i := range self.mineList.length()
|
|
351
|
-
|
|
351
|
+
sp := spawn(self.rng, ship.x, ship.y, 220.0, 90.0)
|
|
352
|
+
self.rng = sp.rng
|
|
353
|
+
self.mineList.at(i).place(sp.at)
|
|
352
354
|
self.sync()
|
|
353
355
|
|
|
354
356
|
# Copies the score, best score, lives and game-over panel into the HUD actors.
|
|
@@ -382,7 +384,9 @@ record World(
|
|
|
382
384
|
orb := self.orbList.at(i)
|
|
383
385
|
if overlaps(ship.x, ship.y, Rules.ShipRadius, orb.x, orb.y, Rules.OrbRadius)
|
|
384
386
|
self.score = self.score + Rules.OrbPoints
|
|
385
|
-
|
|
387
|
+
sp := spawn(self.rng, ship.x, ship.y, 120.0, 40.0)
|
|
388
|
+
self.rng = sp.rng
|
|
389
|
+
orb.place(sp.at)
|
|
386
390
|
while self.mineList.length() < mineCount(self.score)
|
|
387
391
|
self.addMine()
|
|
388
392
|
for i := range self.mineList.length()
|
|
@@ -442,7 +446,7 @@ fun musicVolume() -> Float =
|
|
|
442
446
|
0.3
|
|
443
447
|
|
|
444
448
|
export fun init(io: Io) -> World =
|
|
445
|
-
w := World.create(
|
|
449
|
+
w := World.create(io.randomSeed(), io.textureLoad("sprites.png"))
|
|
446
450
|
w.pickupSound = io.soundLoad("pickup.wav")
|
|
447
451
|
w.hitSound = io.soundLoad("hit.wav")
|
|
448
452
|
w.overSound = io.soundLoad("gameover.wav")
|
|
@@ -470,7 +474,7 @@ fun near(a: Float, b: Float) -> Bool =
|
|
|
470
474
|
{a - b}.abs() < 0.01
|
|
471
475
|
|
|
472
476
|
test "steering accelerates and drag slows the ship"
|
|
473
|
-
ship := Ship(x: 400.0, y: 300.0, tex: -1)
|
|
477
|
+
ship := Ship(x: 400.0, y: 300.0, tex: -1, vx: 0.0, vy: 0.0, ax: 0.0, ay: 0.0, hurt: 0.0, clock: 0.0, live: true)
|
|
474
478
|
ship.ax = 1.0
|
|
475
479
|
ship.act(0.1)
|
|
476
480
|
assert ship.vx > 0.0
|
|
@@ -482,7 +486,7 @@ test "steering accelerates and drag slows the ship"
|
|
|
482
486
|
assert ship.vx < fast / 4.0
|
|
483
487
|
|
|
484
488
|
test "the ship stops at the arena walls"
|
|
485
|
-
ship := Ship(x: 20.0, y: 20.0, tex: -1)
|
|
489
|
+
ship := Ship(x: 20.0, y: 20.0, tex: -1, vx: 0.0, vy: 0.0, ax: 0.0, ay: 0.0, hurt: 0.0, clock: 0.0, live: true)
|
|
486
490
|
ship.ax = -1.0
|
|
487
491
|
ship.ay = -1.0
|
|
488
492
|
for i := range 20
|
plum-examples/games/imperative/scene.plum
CHANGED
|
@@ -48,7 +48,7 @@ record Orbit(Actor, speed: Float) =
|
|
|
48
48
|
self.actChildren(dt)
|
|
49
49
|
|
|
50
50
|
# A sprite that turns on its own axis, `speed` radians per second.
|
|
51
|
-
record Spinner(Actor(width, height, originX, originY), speed: Float, tex: Int, srcX: Float, srcY: Float) =
|
|
51
|
+
record Spinner(Actor(width, height, originX, originY), speed: Float, tex: Int, mut srcX: Float, srcY: Float) =
|
|
52
52
|
|
|
53
53
|
fun init(self) =
|
|
54
54
|
self.srcW: Float = Layout.Cell
|
|
@@ -98,10 +98,10 @@ record Sky(
|
|
|
98
98
|
sun: Spinner,
|
|
99
99
|
label: Text,
|
|
100
100
|
crowd: Group,
|
|
101
|
-
front: Actor?,
|
|
101
|
+
mut front: Actor?,
|
|
102
|
-
selected: Int,
|
|
102
|
+
mut selected: Int,
|
|
103
|
-
clock: Float,
|
|
103
|
+
mut clock: Float,
|
|
104
|
-
paused: Bool,
|
|
104
|
+
mut paused: Bool,
|
|
105
105
|
map: Int)
|
|
106
106
|
|
|
107
107
|
# A body sprite and its (hidden) halo in a group `slot` centred on the group's own
|
|
@@ -273,12 +273,11 @@ record ProbeSpinner(Actor) =
|
|
|
273
273
|
self.actChildren(dt)
|
|
274
274
|
|
|
275
275
|
# A walker: moves along x in its own `act` and counts the frames it draws itself in.
|
|
276
|
-
record ProbeWalker(Actor, speed: Float) =
|
|
276
|
+
record ProbeWalker(Actor, speed: Float, mut drawn: Int) =
|
|
277
277
|
|
|
278
278
|
fun init(self) =
|
|
279
279
|
self.width = 10.0
|
|
280
280
|
self.height = 10.0
|
|
281
|
-
self.drawn = 0
|
|
282
281
|
|
|
283
282
|
override fun act(self, dt: Float) =
|
|
284
283
|
self.x = self.x + self.speed * dt
|
|
@@ -399,7 +398,7 @@ test "act runs behaviours depth first, including invisible actors"
|
|
|
399
398
|
|
|
400
399
|
test "walker_record: a Walker in a Stage moves via its own act and draws via its own draw"
|
|
401
400
|
stage := Stage.create()
|
|
402
|
-
w := ProbeWalker(speed: 4.0)
|
|
401
|
+
w := ProbeWalker(speed: 4.0, drawn: 0)
|
|
403
402
|
stage.root.add(w)
|
|
404
403
|
stage.root.add(Group())
|
|
405
404
|
stage.act(0.5)
|
plum-examples/games/pure/game_pure_scene.plum
CHANGED
|
@@ -29,6 +29,7 @@ import std/game/pure/Actions
|
|
|
29
29
|
import std/game/pure/PureScene
|
|
30
30
|
import std/game/pure/SceneTree
|
|
31
31
|
import std/game/pure/SceneInput
|
|
32
|
+
import std/Io
|
|
32
33
|
|
|
33
34
|
primitive Rules =
|
|
34
35
|
ArenaW = 800.0
|
|
@@ -75,34 +76,43 @@ fun frameOf(clock: Float, count: Int, fps: Float) -> Int =
|
|
|
75
76
|
Int(clock * fps) % count
|
|
76
77
|
|
|
77
78
|
# A random spot at least `gap` away from (ax, ay), drifting at up to `speed`.
|
|
79
|
+
# A Motion and the generator to continue from.
|
|
80
|
+
record Spawned(at: Motion, rng: Random)
|
|
81
|
+
|
|
78
|
-
fun spawn(rng: Random, ax: Float, ay: Float, gap: Float, speed: Float) ->
|
|
82
|
+
fun spawn(rng: Random, ax: Float, ay: Float, gap: Float, speed: Float) -> Spawned =
|
|
79
83
|
margin := 30.0
|
|
80
|
-
|
|
84
|
+
bx := 0.0
|
|
85
|
+
by := 0.0
|
|
86
|
+
r := rng
|
|
81
87
|
tries := 0
|
|
82
88
|
while tries < 20
|
|
89
|
+
dx := r.nextFloat()
|
|
90
|
+
dy := dx.rng.nextFloat()
|
|
91
|
+
r = dy.rng
|
|
83
|
-
|
|
92
|
+
bx = margin + dx.value * {Rules.ArenaW - 2.0 * margin}
|
|
84
|
-
|
|
93
|
+
by = margin + dy.value * {Rules.ArenaH - 2.0 * margin}
|
|
85
|
-
if Math.hypot(
|
|
94
|
+
if Math.hypot(bx - ax, by - ay) >= gap
|
|
86
95
|
break
|
|
87
96
|
tries = tries + 1
|
|
97
|
+
dh := r.nextFloat()
|
|
98
|
+
dp := dh.rng.nextFloat()
|
|
88
|
-
heading :=
|
|
99
|
+
heading := dh.value * Float.Tau
|
|
89
|
-
pace := speed * {0.4 + 0.6 *
|
|
100
|
+
pace := speed * {0.4 + 0.6 * dp.value}
|
|
90
|
-
b.vx = Math.cos(heading) * pace
|
|
91
|
-
|
|
101
|
+
Spawned(at: Motion(x: bx, y: by, vx: Math.cos(heading) * pace, vy: Math.sin(heading) * pace), rng: dp.rng)
|
|
92
|
-
b
|
|
93
102
|
|
|
94
103
|
# `b` moved for `dt` seconds and bounced off the arena walls; `b` itself is unchanged.
|
|
95
104
|
fun driftOf(b: Motion, r: Float, dt: Float) -> Motion =
|
|
96
|
-
n := Motion(..b)
|
|
97
|
-
|
|
105
|
+
x := b.x + b.vx * dt
|
|
98
|
-
|
|
106
|
+
y := b.y + b.vy * dt
|
|
107
|
+
vx := b.vx
|
|
108
|
+
vy := b.vy
|
|
99
|
-
if
|
|
109
|
+
if x < r || x > Rules.ArenaW - r
|
|
100
|
-
|
|
110
|
+
vx = 0.0 - b.vx
|
|
101
|
-
|
|
111
|
+
x = x.max(r).min(Rules.ArenaW - r)
|
|
102
|
-
if
|
|
112
|
+
if y < r || y > Rules.ArenaH - r
|
|
103
|
-
|
|
113
|
+
vy = 0.0 - b.vy
|
|
104
|
-
|
|
114
|
+
y = y.max(r).min(Rules.ArenaH - r)
|
|
105
|
-
|
|
115
|
+
Motion(x: x, y: y, vx: vx, vy: vy)
|
|
106
116
|
|
|
107
117
|
# How many mines are out: more as the score grows.
|
|
108
118
|
fun mineCount(score: Int) -> Int =
|
|
@@ -113,31 +123,29 @@ record ShipState(x: Float, y: Float, vx: Float, vy: Float, rotation: Float, hurt
|
|
|
113
123
|
|
|
114
124
|
# Accelerates by the input axes, applies drag and keeps the ship in the arena.
|
|
115
125
|
fun shipStep(s: ShipState, ax: Float, ay: Float, dt: Float) -> ShipState =
|
|
116
|
-
n := ShipState(..s)
|
|
117
|
-
|
|
126
|
+
hurt := {s.hurt - dt}.max(0.0)
|
|
118
|
-
n.vx = s.vx + ax * Rules.Thrust * dt
|
|
119
|
-
n.vy = s.vy + ay * Rules.Thrust * dt
|
|
120
127
|
keep := {1.0 - Rules.Drag * dt}.max(0.0)
|
|
121
|
-
|
|
128
|
+
vx := {s.vx + ax * Rules.Thrust * dt} * keep
|
|
122
|
-
|
|
129
|
+
vy := {s.vy + ay * Rules.Thrust * dt} * keep
|
|
123
|
-
|
|
130
|
+
x := s.x + vx * dt
|
|
124
|
-
|
|
131
|
+
y := s.y + vy * dt
|
|
125
132
|
r := Rules.ShipRadius
|
|
126
|
-
if
|
|
133
|
+
if x < r
|
|
127
|
-
|
|
134
|
+
x = r
|
|
128
|
-
|
|
135
|
+
vx = 0.0
|
|
129
|
-
if
|
|
136
|
+
if x > Rules.ArenaW - r
|
|
130
|
-
|
|
137
|
+
x = Rules.ArenaW - r
|
|
131
|
-
|
|
138
|
+
vx = 0.0
|
|
132
|
-
if
|
|
139
|
+
if y < r
|
|
133
|
-
|
|
140
|
+
y = r
|
|
134
|
-
|
|
141
|
+
vy = 0.0
|
|
135
|
-
if
|
|
142
|
+
if y > Rules.ArenaH - r
|
|
136
|
-
|
|
143
|
+
y = Rules.ArenaH - r
|
|
137
|
-
|
|
144
|
+
vy = 0.0
|
|
145
|
+
rotation := s.rotation
|
|
138
|
-
if
|
|
146
|
+
if vx * vx + vy * vy > 400.0
|
|
139
|
-
|
|
147
|
+
rotation = Math.atan2(vx, 0.0 - vy)
|
|
140
|
-
|
|
148
|
+
ShipState(x: x, y: y, vx: vx, vy: vy, rotation: rotation, hurt: hurt)
|
|
141
149
|
|
|
142
150
|
fun newShip() -> ShipState =
|
|
143
151
|
ShipState(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, rotation: 0.0, hurt: 0.0)
|
|
@@ -157,34 +165,31 @@ record Panel(Actor) =
|
|
|
157
165
|
|
|
158
166
|
fun create(id: Int, x: Float, y: Float, w: Float, h: Float, c: Color) -> Node =
|
|
159
167
|
n := PureScene.custom(id, x, y, w, h, Panel())
|
|
160
|
-
|
|
168
|
+
Node(..n, color: c)
|
|
161
|
-
n
|
|
162
169
|
|
|
163
|
-
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
170
|
+
override fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
164
171
|
p := m.apply(Vec2(x: 0.0, y: 0.0))
|
|
165
|
-
drawRect(p.x, p.y, n.width * m.scaleX(), n.height * m.scaleY(), tint)
|
|
172
|
+
io.drawRect(p.x, p.y, n.width * m.scaleX(), n.height * m.scaleY(), tint)
|
|
166
173
|
|
|
167
174
|
# The player: only a picture that follows `ShipState`; `act` runs its animation clock.
|
|
168
|
-
record Ship(Actor, clock: Float, tex: Int) =
|
|
175
|
+
record Ship(Actor, mut clock: Float, tex: Int) =
|
|
169
176
|
|
|
170
177
|
fun create(sheet: Int, s: ShipState) -> Node =
|
|
171
178
|
n := PureScene.custom(Id.Ship, s.x - Rules.Half, s.y - Rules.Half, Rules.Size, Rules.Size, Ship(clock: 0.0, tex: sheet))
|
|
172
|
-
|
|
179
|
+
Node(..n, originX: Rules.Half, originY: Rules.Half)
|
|
173
|
-
n.originY = Rules.Half
|
|
174
|
-
n
|
|
175
180
|
|
|
176
|
-
override fun act(self,
|
|
181
|
+
override fun act(self, n: Node, dt: Float) -> Node =
|
|
177
182
|
self.clock = self.clock + dt
|
|
178
183
|
n
|
|
179
184
|
|
|
180
|
-
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
185
|
+
override fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
181
186
|
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
182
|
-
drawCircle(c.x, c.y, 17.0 * m.scaleX(), Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12 * tint.a))
|
|
187
|
+
io.drawCircle(c.x, c.y, 17.0 * m.scaleX(), Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12 * tint.a))
|
|
183
188
|
frame := Float(frameOf(self.clock, 4, 14.0))
|
|
184
|
-
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 0.0, Rules.Cell, Rules.Cell)
|
|
189
|
+
pureDrawSprite(io, m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 0.0, Rules.Cell, Rules.Cell)
|
|
185
190
|
|
|
186
191
|
# A glowing orb drifting about. `phase` offsets its animation from the other orbs.
|
|
187
|
-
record Orb(Actor, vx: Float, vy: Float, phase: Float, clock: Float, tex: Int) =
|
|
192
|
+
record Orb(Actor, mut vx: Float, mut vy: Float, phase: Float, mut clock: Float, tex: Int) =
|
|
188
193
|
|
|
189
194
|
fun create(sheet: Int, phase: Float, id: Int, at: Motion) -> Node =
|
|
190
195
|
Orb.createAt(sheet, phase, id, at, 0.0)
|
|
@@ -193,31 +198,27 @@ record Orb(Actor, vx: Float, vy: Float, phase: Float, clock: Float, tex: Int) =
|
|
|
193
198
|
fun createAt(sheet: Int, phase: Float, id: Int, at: Motion, clock: Float) -> Node =
|
|
194
199
|
n := PureScene.custom(id, at.x - Rules.Half, at.y - Rules.Half, Rules.Size, Rules.Size,
|
|
195
200
|
Orb(vx: at.vx, vy: at.vy, phase: phase, clock: clock, tex: sheet))
|
|
196
|
-
|
|
201
|
+
Node(..n, originX: Rules.Half, originY: Rules.Half)
|
|
197
|
-
n.originY = Rules.Half
|
|
198
|
-
n
|
|
199
202
|
|
|
200
|
-
override fun act(self,
|
|
203
|
+
override fun act(self, n: Node, dt: Float) -> Node =
|
|
201
204
|
m := driftOf(Motion(x: n.x + Rules.Half, y: n.y + Rules.Half, vx: self.vx, vy: self.vy), Rules.OrbRadius, dt)
|
|
202
205
|
self.clock = self.clock + dt
|
|
203
|
-
n.x = m.x - Rules.Half
|
|
204
|
-
n.y = m.y - Rules.Half
|
|
205
206
|
self.vx = m.vx
|
|
206
207
|
self.vy = m.vy
|
|
207
|
-
n
|
|
208
|
+
Node(..n, x: m.x - Rules.Half, y: m.y - Rules.Half)
|
|
208
209
|
|
|
209
|
-
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
210
|
+
override fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
210
211
|
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
211
212
|
k := m.scaleX()
|
|
212
|
-
blendMode(Blend.Add)
|
|
213
|
+
io.blendMode(Blend.Add)
|
|
213
|
-
drawCircle(c.x, c.y, 26.0 * k, Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5 * tint.a))
|
|
214
|
+
io.drawCircle(c.x, c.y, 26.0 * k, Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5 * tint.a))
|
|
214
|
-
drawCircle(c.x, c.y, 17.0 * k, Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5 * tint.a))
|
|
215
|
+
io.drawCircle(c.x, c.y, 17.0 * k, Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5 * tint.a))
|
|
215
|
-
blendMode(Blend.Normal)
|
|
216
|
+
io.blendMode(Blend.Normal)
|
|
216
217
|
frame := Float(frameOf(self.clock + self.phase, 4, 8.0))
|
|
217
|
-
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, Rules.Cell, Rules.Cell, Rules.Cell)
|
|
218
|
+
pureDrawSprite(io, m, tint, n.width, n.height, self.tex, frame * Rules.Cell, Rules.Cell, Rules.Cell, Rules.Cell)
|
|
218
219
|
|
|
219
220
|
# A mine drifting about, turning `spin` radians per second (0 once the game is over).
|
|
220
|
-
record Mine(Actor, vx: Float, vy: Float, spin: Float, clock: Float, tex: Int) =
|
|
221
|
+
record Mine(Actor, mut vx: Float, mut vy: Float, spin: Float, mut clock: Float, tex: Int) =
|
|
221
222
|
|
|
222
223
|
fun create(sheet: Int, id: Int, at: Motion, spin: Float) -> Node =
|
|
223
224
|
Mine.createAt(sheet, id, at, spin, 0.0)
|
|
@@ -225,41 +226,36 @@ record Mine(Actor, vx: Float, vy: Float, spin: Float, clock: Float, tex: Int) =
|
|
|
225
226
|
fun createAt(sheet: Int, id: Int, at: Motion, spin: Float, clock: Float) -> Node =
|
|
226
227
|
n := PureScene.custom(id, at.x - Rules.Half, at.y - Rules.Half, Rules.Size, Rules.Size,
|
|
227
228
|
Mine(vx: at.vx, vy: at.vy, spin: spin, clock: clock, tex: sheet))
|
|
228
|
-
|
|
229
|
+
Node(..n, originX: Rules.Half, originY: Rules.Half)
|
|
229
|
-
n.originY = Rules.Half
|
|
230
|
-
n
|
|
231
230
|
|
|
232
|
-
override fun act(self,
|
|
231
|
+
override fun act(self, n: Node, dt: Float) -> Node =
|
|
233
232
|
m := driftOf(Motion(x: n.x + Rules.Half, y: n.y + Rules.Half, vx: self.vx, vy: self.vy), Rules.MineRadius, dt)
|
|
234
233
|
self.clock = self.clock + dt
|
|
235
|
-
n.x = m.x - Rules.Half
|
|
236
|
-
n.y = m.y - Rules.Half
|
|
237
234
|
self.vx = m.vx
|
|
238
235
|
self.vy = m.vy
|
|
239
|
-
n.
|
|
236
|
+
Node(..n, x: m.x - Rules.Half, y: m.y - Rules.Half, rotation: n.rotation + self.spin * dt)
|
|
240
|
-
n
|
|
241
237
|
|
|
242
|
-
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
238
|
+
override fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
243
239
|
frame := Float(frameOf(self.clock, 4, 4.0))
|
|
244
|
-
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 2.0 * Rules.Cell, Rules.Cell, Rules.Cell)
|
|
240
|
+
pureDrawSprite(io, m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 2.0 * Rules.Cell, Rules.Cell, Rules.Cell)
|
|
245
241
|
|
|
246
242
|
# A background star that twinkles.
|
|
247
|
-
record Star(Actor, seed: Float, clock: Float) =
|
|
243
|
+
record Star(Actor, seed: Float, mut clock: Float) =
|
|
248
244
|
|
|
249
245
|
fun create(i: Int) -> Node =
|
|
250
246
|
r := 1.0 + Float(i % 3) * 0.5
|
|
251
247
|
PureScene.custom(Id.Star + i, Float({i * 7919} % 800) - r, Float({i * 104729} % 600) - r, r * 2.0, r * 2.0,
|
|
252
248
|
Star(seed: Float(i), clock: 0.0))
|
|
253
249
|
|
|
254
|
-
override fun act(self,
|
|
250
|
+
override fun act(self, n: Node, dt: Float) -> Node =
|
|
255
251
|
self.clock = self.clock + dt
|
|
256
252
|
n
|
|
257
253
|
|
|
258
|
-
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
254
|
+
override fun draw(self, io: Io, n: Node, m: Affine, tint: Color) =
|
|
259
255
|
twinkle := 0.25 + 0.2 * Math.sin(self.clock * 2.0 + self.seed)
|
|
260
256
|
r := n.width / 2.0
|
|
261
257
|
c := m.apply(Vec2(x: r, y: r))
|
|
262
|
-
drawCircle(c.x, c.y, r * m.scaleX(), Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle * tint.a))
|
|
258
|
+
io.drawCircle(c.x, c.y, r * m.scaleX(), Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle * tint.a))
|
|
263
259
|
|
|
264
260
|
# --- reading and changing the tree ---
|
|
265
261
|
|
|
@@ -279,12 +275,11 @@ fun find(root: Node, id: Int) -> Node =
|
|
|
279
275
|
fun kidsOf(n: Node) -> Slice[Node] =
|
|
280
276
|
PureScene.childrenOf(n) ?: crash "not a group"
|
|
281
277
|
|
|
282
|
-
fun setVisible(
|
|
278
|
+
fun setVisible(n: Node, v: Bool) -> Node =
|
|
283
|
-
|
|
279
|
+
Node(..n, visible: v)
|
|
284
|
-
n
|
|
285
280
|
|
|
286
281
|
# The group `g` with `k` added as its last child.
|
|
287
|
-
fun addKid(
|
|
282
|
+
fun addKid(g: Node, k: Node) -> Node =
|
|
288
283
|
ks := kidsOf(g)
|
|
289
284
|
ks.push(k)
|
|
290
285
|
g
|
|
@@ -308,18 +303,33 @@ fun textOf(n: Node) -> Str =
|
|
|
308
303
|
|
|
309
304
|
fun hudLabel(id: Int, text: Str, x: Float, y: Float, size: Float, c: Color) -> Node =
|
|
310
305
|
l := PureScene.label(id, text, x, y, size)
|
|
311
|
-
|
|
306
|
+
Node(..l, color: c)
|
|
312
|
-
l
|
|
313
307
|
|
|
314
308
|
fun icon(sheet: Int, i: Int) -> Node =
|
|
315
309
|
s := PureScene.sprite(Id.Icon + i, sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 32.0)
|
|
316
|
-
|
|
310
|
+
Node(..s, kind: Sprite(tex: sheet, srcX: 0.0, srcY: 0.0, srcW: Rules.Cell, srcH: Rules.Cell))
|
|
317
|
-
s
|
|
318
311
|
|
|
319
312
|
# The whole scene: backdrop, stars, mines, orbs, ship and HUD layers, in drawing order.
|
|
320
313
|
# The layer groups are 0 x 0 so they never swallow a click; the HUD holds the
|
|
321
314
|
# game-over overlay with its restart button.
|
|
315
|
+
# The starting spots of the orbs and mines, and the generator to continue from.
|
|
316
|
+
record Spots(orbs: Slice[Motion], mines: Slice[Motion], rng: Random)
|
|
317
|
+
|
|
318
|
+
fun startSpots(rng: Random, ship: ShipState) -> Spots =
|
|
319
|
+
orbs := Slice[Motion]()
|
|
320
|
+
mines := Slice[Motion]()
|
|
321
|
+
r := rng
|
|
322
|
+
for i := range Rules.OrbCount
|
|
323
|
+
sp := spawn(r, ship.x, ship.y, 120.0, 40.0)
|
|
324
|
+
r = sp.rng
|
|
325
|
+
orbs.push(sp.at)
|
|
326
|
+
for i := range mineCount(0)
|
|
327
|
+
sp := spawn(r, ship.x, ship.y, 220.0, 90.0)
|
|
328
|
+
r = sp.rng
|
|
329
|
+
mines.push(sp.at)
|
|
330
|
+
Spots(orbs: orbs, mines: mines, rng: r)
|
|
331
|
+
|
|
322
|
-
fun buildRoot(
|
|
332
|
+
fun buildRoot(spots: Spots, sheet: Int, best: Int) -> Node =
|
|
323
333
|
ship := newShip()
|
|
324
334
|
stars := Slice[Node]()
|
|
325
335
|
for i := range 40
|
|
@@ -327,19 +337,18 @@ fun buildRoot(rng: Random, sheet: Int, best: Int) -> Node =
|
|
|
327
337
|
mines := Slice[Node]()
|
|
328
338
|
orbs := Slice[Node]()
|
|
329
339
|
for i := range Rules.OrbCount
|
|
330
|
-
orbs.push(Orb.create(sheet, Float(i) * 0.1, Id.Orb + i,
|
|
340
|
+
orbs.push(Orb.create(sheet, Float(i) * 0.1, Id.Orb + i, spots.orbs.at(i)))
|
|
331
341
|
for i := range mineCount(0)
|
|
332
|
-
mines.push(Mine.create(sheet, Id.Mine + i,
|
|
342
|
+
mines.push(Mine.create(sheet, Id.Mine + i, spots.mines.at(i), 2.0))
|
|
333
|
-
|
|
343
|
+
b0 := PureScene.button(Id.Restart, "Again", sheet, sheet, 340.0, 342.0, 120.0, 44.0)
|
|
334
|
-
btn
|
|
344
|
+
btn := Node(..b0, color: Color(r: 0.5, g: 0.8, b: 1.0, a: 0.9), visible: false)
|
|
335
|
-
btn.visible = false
|
|
336
345
|
over := Slice(
|
|
337
346
|
Panel.create(Id.OverlayPanel, 0.0, 230.0, Rules.ArenaW, 170.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
338
347
|
hudLabel(Id.OverTitle, "Game over", 300.0, 244.0, 48.0, Color.Red),
|
|
339
348
|
hudLabel(Id.OverHint, "Press Enter to play again", 270.0, 300.0, 24.0, Color.White),
|
|
340
349
|
btn)
|
|
341
|
-
|
|
350
|
+
o0 := PureScene.group(Id.Overlay, 0.0, 0.0, 0.0, 0.0, over)
|
|
342
|
-
overlay
|
|
351
|
+
overlay := Node(..o0, visible: false)
|
|
343
352
|
hud := Slice(
|
|
344
353
|
hudLabel(Id.Score, "Score 0", 16.0, 12.0, 28.0, Color.White),
|
|
345
354
|
hudLabel(Id.Best, "Best {best}", 16.0, 44.0, 20.0, Color.Yellow))
|
|
@@ -360,32 +369,41 @@ fun buildRoot(rng: Random, sheet: Int, best: Int) -> Node =
|
|
|
360
369
|
# The host-facing shell: the current pure `Stage` plus the run's state. It is the
|
|
361
370
|
# only thing that is replaced from frame to frame.
|
|
362
371
|
record World(
|
|
363
|
-
stage: Stage,
|
|
372
|
+
mut stage: Stage,
|
|
364
|
-
ship: ShipState,
|
|
373
|
+
mut ship: ShipState,
|
|
365
|
-
rng: Random,
|
|
374
|
+
mut rng: Random,
|
|
366
|
-
mines: Int,
|
|
375
|
+
mut mines: Int,
|
|
367
|
-
score: Int,
|
|
376
|
+
mut score: Int,
|
|
368
|
-
best: Int,
|
|
377
|
+
mut best: Int,
|
|
369
|
-
lives: Int,
|
|
378
|
+
mut lives: Int,
|
|
370
|
-
over: Bool,
|
|
379
|
+
mut over: Bool,
|
|
371
|
-
clock: Float,
|
|
380
|
+
mut clock: Float,
|
|
372
|
-
wasDown: Bool,
|
|
381
|
+
mut wasDown: Bool,
|
|
373
382
|
sheet: Int,
|
|
374
|
-
pickupSound: Int,
|
|
383
|
+
mut pickupSound: Int,
|
|
375
|
-
hitSound: Int,
|
|
384
|
+
mut hitSound: Int,
|
|
376
|
-
overSound: Int,
|
|
385
|
+
mut overSound: Int,
|
|
377
|
-
musicSound: Int)
|
|
386
|
+
mut musicSound: Int)
|
|
378
387
|
|
|
379
388
|
fun newWorld(seed: Int, sheet: Int) -> World =
|
|
389
|
+
ship := newShip()
|
|
380
|
-
|
|
390
|
+
spots := startSpots(Random.seeded(seed), ship)
|
|
381
|
-
|
|
391
|
+
next := spots.rng
|
|
382
392
|
World(
|
|
383
|
-
stage: SceneInput.stage(
|
|
393
|
+
stage: SceneInput.stage(buildRoot(spots, sheet, 0)), ship: ship, rng: next, mines: mineCount(0), score: 0, best: 0,
|
|
384
394
|
lives: Rules.StartLives, over: false, clock: 0.0, wasDown: false, sheet: sheet,
|
|
385
395
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
386
396
|
|
|
397
|
+
fun modifyRoot(st: Stage, id: Int, f: fn(Node) -> Node) -> Stage =
|
|
398
|
+
match st
|
|
399
|
+
Stage(root, camera, captured) => Stage(root: SceneTree.modify(root, id, f), camera: camera, captured: captured)
|
|
400
|
+
|
|
401
|
+
fun withCamera(st: Stage, camera: Affine) -> Stage =
|
|
402
|
+
match st
|
|
403
|
+
Stage(root, _, captured) => Stage(root: root, camera: camera, captured: captured)
|
|
404
|
+
|
|
387
405
|
fun swap(w: World, id: Int, f: fn(Node) -> Node) =
|
|
388
|
-
w.stage
|
|
406
|
+
w.stage = modifyRoot(w.stage, id, f)
|
|
389
407
|
|
|
390
408
|
# Starts a fresh game, keeping the best score, the camera and the random stream.
|
|
391
409
|
fun restart(w: World) =
|
|
@@ -395,7 +413,10 @@ fun restart(w: World) =
|
|
|
395
413
|
w.over = false
|
|
396
414
|
w.ship = newShip()
|
|
397
415
|
w.mines = mineCount(0)
|
|
416
|
+
spots := startSpots(w.rng, w.ship)
|
|
417
|
+
next := spots.rng
|
|
418
|
+
w.rng = next
|
|
398
|
-
w.stage = Stage(root: buildRoot(
|
|
419
|
+
w.stage = Stage(root: buildRoot(spots, w.sheet, w.best), camera: w.stage.camera, captured: null)
|
|
399
420
|
|
|
400
421
|
# Copies the score, best score, lives and game-over panel into the HUD actors.
|
|
401
422
|
fun syncHud(w: World) =
|
|
@@ -434,8 +455,7 @@ fun mineActor(sheet: Int, id: Int, at: Motion, spin: Float) -> Node =
|
|
|
434
455
|
|
|
435
456
|
fun stillMine(sheet: Int, id: Int, a: Node) -> Node =
|
|
436
457
|
m := Mine.createAt(sheet, id, Motion(x: cx(a), y: cy(a), vx: 0.0, vy: 0.0), 0.0, clockOf(a))
|
|
437
|
-
|
|
458
|
+
Node(..m, rotation: a.rotation)
|
|
438
|
-
m
|
|
439
459
|
|
|
440
460
|
# Moves the ship node to the ship state; it blinks while it is hurt, and is hidden once the game is over.
|
|
441
461
|
fun showShip(w: World) =
|
|
@@ -444,18 +464,14 @@ fun showShip(w: World) =
|
|
|
444
464
|
s := w.ship
|
|
445
465
|
swap(w, Id.Ship, |a| shipActor(a, s, visible))
|
|
446
466
|
|
|
447
|
-
fun shipActor(
|
|
467
|
+
fun shipActor(a: Node, s: ShipState, visible: Bool) -> Node =
|
|
448
|
-
a.x = s.x - Rules.Half
|
|
449
|
-
a.y = s.y - Rules.Half
|
|
450
|
-
a.
|
|
468
|
+
Node(..a, x: s.x - Rules.Half, y: s.y - Rules.Half, rotation: s.rotation, visible: visible)
|
|
451
|
-
a.visible = visible
|
|
452
|
-
a
|
|
453
469
|
|
|
454
470
|
fun overlaps(ax: Float, ay: Float, ra: Float, bx: Float, by: Float, rb: Float) -> Bool =
|
|
455
471
|
Math.hypot(ax - bx, ay - by) < ra + rb
|
|
456
472
|
|
|
457
473
|
# The stage after `inputs` and `dt`, appending the events it raised to `events`.
|
|
458
|
-
fun frameInto(
|
|
474
|
+
fun frameInto(s: Stage, dt: Float, inputs: Slice[Input], events: Slice[UiEvent]) -> Stage =
|
|
459
475
|
h := s.frame(dt, inputs)
|
|
460
476
|
for i := range h.events.length()
|
|
461
477
|
events.push(h.events.at(i))
|
|
@@ -483,7 +499,9 @@ fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: Slice[Input]) ->
|
|
|
483
499
|
if overlaps(ship.x, ship.y, Rules.ShipRadius, cx(orb), cy(orb), Rules.OrbRadius)
|
|
484
500
|
w.score = w.score + Rules.OrbPoints
|
|
485
501
|
changed = true
|
|
486
|
-
|
|
502
|
+
sp := spawn(w.rng, ship.x, ship.y, 120.0, 40.0)
|
|
503
|
+
w.rng = sp.rng
|
|
504
|
+
at := sp.at
|
|
487
505
|
sheet := w.sheet
|
|
488
506
|
id := Id.Orb + i
|
|
489
507
|
phase := Float(i) * 0.1
|
|
@@ -491,7 +509,9 @@ fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: Slice[Input]) ->
|
|
|
491
509
|
while w.mines < mineCount(w.score)
|
|
492
510
|
sheet := w.sheet
|
|
493
511
|
id := Id.Mine + w.mines
|
|
494
|
-
|
|
512
|
+
sp := spawn(w.rng, ship.x, ship.y, 220.0, 90.0)
|
|
513
|
+
w.rng = sp.rng
|
|
514
|
+
at := sp.at
|
|
495
515
|
swap(w, Id.MineLayer, |g| addKid(g, Mine.create(sheet, id, at, 2.0)))
|
|
496
516
|
w.mines = w.mines + 1
|
|
497
517
|
for i := range w.mines
|
|
@@ -499,9 +519,7 @@ fun advance(w: World, ax: Float, ay: Float, dt: Float, inputs: Slice[Input]) ->
|
|
|
499
519
|
if w.ship.hurt <= 0.0 && overlaps(ship.x, ship.y, Rules.ShipRadius, cx(mine), cy(mine), Rules.MineRadius)
|
|
500
520
|
w.lives = w.lives - 1
|
|
501
521
|
cur := w.ship
|
|
502
|
-
s := ShipState(..cur)
|
|
503
|
-
|
|
522
|
+
w.ship = ShipState(..cur, hurt: Rules.HurtTime)
|
|
504
|
-
w.ship = s
|
|
505
523
|
changed = true
|
|
506
524
|
if w.lives <= 0
|
|
507
525
|
w.over = true
|
|
@@ -523,7 +541,7 @@ fun fitCamera(sw: Float, sh: Float) -> Affine =
|
|
|
523
541
|
Affine(a: zoom, b: 0.0, c: 0.0, d: zoom, e: {sw - zoom * Rules.ArenaW} / 2.0, f: {sh - zoom * Rules.ArenaH} / 2.0)
|
|
524
542
|
|
|
525
543
|
fun fit(w: World, sw: Float, sh: Float) =
|
|
526
|
-
w.stage
|
|
544
|
+
w.stage = withCamera(w.stage, fitCamera(sw, sh))
|
|
527
545
|
|
|
528
546
|
# --- host glue: input, sound, drawing ---
|
|
529
547
|
|
|
@@ -531,17 +549,17 @@ fun musicVolume() -> Float =
|
|
|
531
549
|
0.3
|
|
532
550
|
|
|
533
551
|
# Restarts the music loop from the top.
|
|
534
|
-
fun startMusic(w: World) =
|
|
552
|
+
fun startMusic(io: Io, w: World) =
|
|
535
|
-
soundStop(w.musicSound)
|
|
553
|
+
io.soundStop(w.musicSound)
|
|
536
|
-
soundLoop(w.musicSound, musicVolume())
|
|
554
|
+
io.soundLoop(w.musicSound, musicVolume())
|
|
537
|
-
|
|
555
|
+
|
|
538
|
-
export fun init() -> World =
|
|
556
|
+
export fun init(io: Io) -> World =
|
|
539
|
-
w := newWorld(
|
|
557
|
+
w := newWorld(io.randomSeed(), io.textureLoad("sprites.png"))
|
|
540
|
-
w.pickupSound = soundLoad("pickup.wav")
|
|
558
|
+
w.pickupSound = io.soundLoad("pickup.wav")
|
|
541
|
-
w.hitSound = soundLoad("hit.wav")
|
|
559
|
+
w.hitSound = io.soundLoad("hit.wav")
|
|
542
|
-
w.overSound = soundLoad("gameover.wav")
|
|
560
|
+
w.overSound = io.soundLoad("gameover.wav")
|
|
543
|
-
w.musicSound = soundLoad("music.wav")
|
|
561
|
+
w.musicSound = io.soundLoad("music.wav")
|
|
544
|
-
startMusic(w)
|
|
562
|
+
startMusic(io, w)
|
|
545
563
|
w
|
|
546
564
|
|
|
547
565
|
fun axis(negative: Bool, positive: Bool) -> Float =
|
|
@@ -552,35 +570,35 @@ fun axis(negative: Bool, positive: Bool) -> Float =
|
|
|
552
570
|
else
|
|
553
571
|
0.0
|
|
554
572
|
|
|
555
|
-
export fun update(w: World, dt: Float) =
|
|
573
|
+
export fun update(io: Io, w: World, dt: Float) =
|
|
556
|
-
if w.over && {isKeyPressed(Key.Enter) || isKeyPressed(Key.KeyR)}
|
|
574
|
+
if w.over && {io.isKeyPressed(Key.Enter) || io.isKeyPressed(Key.KeyR)}
|
|
557
575
|
restart(w)
|
|
558
|
-
startMusic(w)
|
|
576
|
+
startMusic(io, w)
|
|
559
|
-
inputs := SceneInput.pollPointer(w.wasDown)
|
|
577
|
+
inputs := SceneInput.pollPointer(io, w.wasDown)
|
|
560
|
-
w.wasDown = isMouseDown(0)
|
|
578
|
+
w.wasDown = io.isMouseDown(0)
|
|
561
|
-
left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
|
|
579
|
+
left := io.isKeyDown(Key.Left) || io.isKeyDown(Key.KeyA)
|
|
562
|
-
right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
|
|
580
|
+
right := io.isKeyDown(Key.Right) || io.isKeyDown(Key.KeyD)
|
|
563
|
-
up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
|
|
581
|
+
up := io.isKeyDown(Key.Up) || io.isKeyDown(Key.KeyW)
|
|
564
|
-
down := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
|
|
582
|
+
down := io.isKeyDown(Key.Down) || io.isKeyDown(Key.KeyS)
|
|
565
583
|
score := w.score
|
|
566
584
|
lives := w.lives
|
|
567
585
|
restarted := advance(w, axis(left, right), axis(up, down), dt, inputs)
|
|
568
586
|
if restarted
|
|
569
|
-
startMusic(w)
|
|
587
|
+
startMusic(io, w)
|
|
570
588
|
return
|
|
571
589
|
if w.score > score
|
|
572
|
-
soundPlay(w.pickupSound, 0.8)
|
|
590
|
+
io.soundPlay(w.pickupSound, 0.8)
|
|
573
591
|
if w.lives < lives
|
|
574
592
|
if w.over
|
|
575
|
-
soundStop(w.musicSound)
|
|
593
|
+
io.soundStop(w.musicSound)
|
|
576
|
-
soundPlay(w.overSound, 1.0)
|
|
594
|
+
io.soundPlay(w.overSound, 1.0)
|
|
577
595
|
else
|
|
578
|
-
soundPlay(w.hitSound, 1.0)
|
|
596
|
+
io.soundPlay(w.hitSound, 1.0)
|
|
579
597
|
|
|
580
|
-
export fun draw(w: World) =
|
|
598
|
+
export fun draw(io: Io, w: World) =
|
|
581
|
-
fit(w, screenWidth(), screenHeight())
|
|
599
|
+
fit(w, io.screenWidth(), io.screenHeight())
|
|
582
|
-
clear(Color.rgb(0.04, 0.05, 0.1))
|
|
600
|
+
io.clear(Color.rgb(0.04, 0.05, 0.1))
|
|
583
|
-
w.stage.draw()
|
|
601
|
+
w.stage.draw(io)
|
|
584
602
|
|
|
585
603
|
# --- tests ---
|
|
586
604
|
|
plum-examples/{tools → games/tools}/make_sounds.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Generates the Orb Run sound effects and music loop into ../assets.
|
|
3
3
|
|
|
4
|
-
Standard library only: python3 plum-examples/tools/make_sounds.py
|
|
4
|
+
Standard library only: python3 plum-examples/games/tools/make_sounds.py
|
|
5
5
|
Everything is synthesized here, so the WAVs are original and reproducible.
|
|
6
6
|
All files are mono, 16-bit, 22050 Hz.
|
|
7
7
|
"""
|
plum-examples/{tools → games/tools}/make_sprites.py
RENAMED
|
File without changes
|
plum-examples/io.plum
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import std/Str
|
|
2
|
-
|
|
3
|
-
# `printLn` mirrors `libs/std/os.plum`'s declaration; redeclared locally
|
|
4
|
-
# (rather than `import std/os`) so this stays the SMALLEST possible example
|
|
5
|
-
# — `import std/os` pulls in every other filesystem extern os.plum declares
|
|
6
|
-
# too, which the Rust-level `ioExampleCompilesAndRunsCorrectly` test (which
|
|
7
|
-
# actually RUNS this module under wasmtime, providing real host imports)
|
|
8
|
-
# would then all need stubs for.
|
|
9
|
-
extern fun printLn(s: Str)
|
|
10
|
-
|
|
11
|
-
fun main() =
|
|
12
|
-
printLn("hello from plum")
|
|
13
|
-
name := "world"
|
|
14
|
-
printLn("hello, {name}!")
|
plum-examples/loops.plum
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Loops: `while` condition loops, `for ... := range` iteration over ranges and
|
|
2
|
-
# collections, with inline filtering and mapping.
|
|
3
|
-
import std/Str
|
|
4
|
-
import std/Int
|
|
5
|
-
import std/Float
|
|
6
|
-
import std/Slice
|
|
7
|
-
import std/Array
|
|
8
|
-
import std/Testing
|
|
9
|
-
import std/Seq
|
|
10
|
-
|
|
11
|
-
fun sumList(xs: Slice[Int]) -> Int =
|
|
12
|
-
total := 0
|
|
13
|
-
for x := range xs
|
|
14
|
-
total = total + x
|
|
15
|
-
return total
|
|
16
|
-
|
|
17
|
-
fun firstOver(xs: Slice[Int], limit: Int) -> Int =
|
|
18
|
-
for x := range xs
|
|
19
|
-
if x > limit
|
|
20
|
-
return x
|
|
21
|
-
return -1
|
|
22
|
-
|
|
23
|
-
test "plain_iterable_for: List and Array"
|
|
24
|
-
xs := Slice(1, 2, 3, 4)
|
|
25
|
-
assert sumList(xs) == 10
|
|
26
|
-
arr := Array.filled(3, 7)
|
|
27
|
-
total := 0
|
|
28
|
-
for a := range arr
|
|
29
|
-
total = total + a
|
|
30
|
-
assert total == 21
|
|
31
|
-
|
|
32
|
-
test "index_value_binders: i, x"
|
|
33
|
-
xs := Slice(10, 20, 30)
|
|
34
|
-
acc := 0
|
|
35
|
-
for i, x := range xs
|
|
36
|
-
acc = acc + i * x
|
|
37
|
-
assert acc == 80
|
|
38
|
-
|
|
39
|
-
test "clause_if_keeps_when_true: if cond keeps the element, false skips it"
|
|
40
|
-
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
41
|
-
total := 0
|
|
42
|
-
for x := range xs, if x % 2 == 0
|
|
43
|
-
total = total + x
|
|
44
|
-
assert total == 12
|
|
45
|
-
|
|
46
|
-
test "clause_bind_scoped: name := expr is visible to later clauses and body"
|
|
47
|
-
xs := Slice(1, 2, 3, 4)
|
|
48
|
-
total := 0
|
|
49
|
-
for x := range xs, sq := x * x, if sq > 9
|
|
50
|
-
total = total + sq
|
|
51
|
-
assert total == 16
|
|
52
|
-
|
|
53
|
-
test "break_continue_return_semantics"
|
|
54
|
-
xs := Slice(1, 2, 3, 4, 5)
|
|
55
|
-
total := 0
|
|
56
|
-
for x := range xs
|
|
57
|
-
if x == 2
|
|
58
|
-
continue
|
|
59
|
-
if x == 5
|
|
60
|
-
break
|
|
61
|
-
total = total + x
|
|
62
|
-
assert total == 8
|
|
63
|
-
assert firstOver(xs, 3) == 4
|
|
64
|
-
assert firstOver(xs, 9) == -1
|
|
65
|
-
|
|
66
|
-
test "nested iterable loops"
|
|
67
|
-
xs := Slice(1, 2)
|
|
68
|
-
ys := Slice(10, 20)
|
|
69
|
-
total := 0
|
|
70
|
-
for x := range xs
|
|
71
|
-
for y := range ys
|
|
72
|
-
total = total + x * y
|
|
73
|
-
assert total == 90
|
|
74
|
-
|
|
75
|
-
test "range_int: 0 to n-1, empty for 0, continue still advances"
|
|
76
|
-
total := 0
|
|
77
|
-
for i := range 5
|
|
78
|
-
total = total + i
|
|
79
|
-
assert total == 10
|
|
80
|
-
n := 0
|
|
81
|
-
for _ := range 0
|
|
82
|
-
n = n + 1
|
|
83
|
-
assert n == 0
|
|
84
|
-
odd := 0
|
|
85
|
-
for i := range 6
|
|
86
|
-
if i % 2 == 0
|
|
87
|
-
continue
|
|
88
|
-
odd = odd + i
|
|
89
|
-
assert odd == 9
|
|
90
|
-
|
|
91
|
-
test "range_until_to_downTo: bounds incl. empty ranges"
|
|
92
|
-
a := 0
|
|
93
|
-
for i := range 2.until(6)
|
|
94
|
-
a = a + i
|
|
95
|
-
assert a == 14
|
|
96
|
-
b := 0
|
|
97
|
-
for i := range 1.to(4)
|
|
98
|
-
b = b + i
|
|
99
|
-
assert b == 10
|
|
100
|
-
c := 0
|
|
101
|
-
for i := range 4.downTo(1)
|
|
102
|
-
c = c * 10 + i
|
|
103
|
-
assert c == 4321
|
|
104
|
-
n := 0
|
|
105
|
-
for _ := range 5.until(5)
|
|
106
|
-
n = n + 1
|
|
107
|
-
for _ := range 1.downTo(2)
|
|
108
|
-
n = n + 1
|
|
109
|
-
for _ := range 3.to(2)
|
|
110
|
-
n = n + 1
|
|
111
|
-
assert n == 0
|
|
112
|
-
|
|
113
|
-
test "range_stepBy: stepBy on until and to"
|
|
114
|
-
total := 0
|
|
115
|
-
for i := range 0.until(10).stepBy(3)
|
|
116
|
-
total = total + i
|
|
117
|
-
assert total == 18
|
|
118
|
-
for i := range 1.to(10).stepBy(4)
|
|
119
|
-
total = total + i
|
|
120
|
-
assert total == 18 + 15
|
|
121
|
-
|
|
122
|
-
test "range_seq_terminal: a terminal over an Int range"
|
|
123
|
-
evens := for i := range 0.until(10).filter()
|
|
124
|
-
i % 2 == 0
|
|
125
|
-
assert evens.length() == 5
|
plum-examples/numbers.plum
CHANGED
|
@@ -1,7 +1,91 @@
|
|
|
1
|
+
# Numbers: Int and Float methods, operators on numbers and constants.
|
|
1
2
|
import std/Str
|
|
2
3
|
import std/Int
|
|
3
4
|
import std/Float
|
|
4
5
|
import std/Math
|
|
6
|
+
import std/Slice
|
|
7
|
+
|
|
8
|
+
# ==== operators ====
|
|
9
|
+
|
|
10
|
+
# Operators on non-primitive operands call methods: `a + b` is `a.add(b)`,
|
|
11
|
+
# `-a` is `a.neg()`, `a == b` is `a.eq(b)`, `a < b` is `a.lt(b)`.
|
|
12
|
+
record Vec2(x: Float, y: Float) =
|
|
13
|
+
|
|
14
|
+
fun toStr(self) -> Str =
|
|
15
|
+
"Vec2({self.x}, {self.y})"
|
|
16
|
+
|
|
17
|
+
fun add(self, o: Vec2) -> Vec2 =
|
|
18
|
+
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
19
|
+
|
|
20
|
+
fun mul(self, s: Float) -> Vec2 =
|
|
21
|
+
Vec2(x: self.x * s, y: self.y * s)
|
|
22
|
+
|
|
23
|
+
fun neg(self) -> Vec2 =
|
|
24
|
+
Vec2(x: 0.0 - self.x, y: 0.0 - self.y)
|
|
25
|
+
|
|
26
|
+
fun eq(self, o: Vec2) -> Bool =
|
|
27
|
+
self.x == o.x && self.y == o.y
|
|
28
|
+
|
|
29
|
+
fun lt(self, o: Vec2) -> Bool =
|
|
30
|
+
self.x < o.x
|
|
31
|
+
|
|
32
|
+
fun le(self, o: Vec2) -> Bool =
|
|
33
|
+
self.x <= o.x
|
|
34
|
+
|
|
35
|
+
record Plain(v: Int)
|
|
36
|
+
|
|
37
|
+
fun double(a: T) -> T =
|
|
38
|
+
a + a
|
|
39
|
+
|
|
40
|
+
# ==== constants ====
|
|
41
|
+
|
|
42
|
+
# Constants and static functions live inside a type's body or a `primitive`
|
|
43
|
+
# namespace — there are no top-level constants. `Name = expr` declares an
|
|
44
|
+
# associated constant, a `fun` without `self` a static function. They are
|
|
45
|
+
# reached as `Type.Name` / `Type.f()` from anywhere, including inside the same
|
|
46
|
+
# body. Constant names are unique per type, not global.
|
|
47
|
+
|
|
48
|
+
# A `primitive` whose name is not a built-in type is a stateless namespace.
|
|
49
|
+
primitive Grid =
|
|
50
|
+
Width = 8
|
|
51
|
+
Height = Grid.Width * 2
|
|
52
|
+
CellScale = 2.5
|
|
53
|
+
Name = "plum"
|
|
54
|
+
DebugDraw = true
|
|
55
|
+
|
|
56
|
+
fun cells() -> Int =
|
|
57
|
+
Grid.Width * Grid.Height
|
|
58
|
+
|
|
59
|
+
record Tile(x: Int) =
|
|
60
|
+
|
|
61
|
+
Width = 100
|
|
62
|
+
|
|
63
|
+
fun offset(self) -> Int = Tile.Width + self.x
|
|
64
|
+
|
|
65
|
+
fun origin() -> Tile =
|
|
66
|
+
Tile(x: 0)
|
|
67
|
+
|
|
68
|
+
record Wrap[T](v: T) =
|
|
69
|
+
|
|
70
|
+
fun area(self) -> Int = Grid.Width * Grid.Height
|
|
71
|
+
|
|
72
|
+
struct ConstVec2(x: Float, y: Float) =
|
|
73
|
+
|
|
74
|
+
Zero = ConstVec2(x: 0.0, y: 0.0)
|
|
75
|
+
|
|
76
|
+
fun scaled(n: Float) -> Float = n * Grid.CellScale
|
|
77
|
+
|
|
78
|
+
primitive Shelf =
|
|
79
|
+
Sides = 4
|
|
80
|
+
Names = Slice("a", "b", "c")
|
|
81
|
+
|
|
82
|
+
fun sidesTwice() -> Int =
|
|
83
|
+
twice(Shelf.Sides)
|
|
84
|
+
|
|
85
|
+
fun twice(n: Int) -> Int =
|
|
86
|
+
n * 2
|
|
87
|
+
|
|
88
|
+
# ==== numbers tests ====
|
|
5
89
|
|
|
6
90
|
# Regression coverage for the `primitive Int` / `primitive Float` methods
|
|
7
91
|
# (`plum-std/Int.plum`, `plum-std/Float.plum`), the `Math` namespace and the
|
|
@@ -106,3 +190,61 @@ test "associated constants live on their type"
|
|
|
106
190
|
assert Float.Tau == 2.0f * Float.Pi
|
|
107
191
|
assert Float.HalfPi * 2.0f == Float.Pi
|
|
108
192
|
assert Float.Max > 1.0e300
|
|
193
|
+
|
|
194
|
+
# ==== operators tests ====
|
|
195
|
+
|
|
196
|
+
test "vec2_add_operator"
|
|
197
|
+
r := Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0)
|
|
198
|
+
assert r.x == 4.0
|
|
199
|
+
assert r.y == 6.0
|
|
200
|
+
|
|
201
|
+
test "mul_scalar_rhs"
|
|
202
|
+
r := Vec2(x: 1.0, y: 2.0) * 2.0
|
|
203
|
+
assert r.x == 2.0
|
|
204
|
+
assert r.y == 4.0
|
|
205
|
+
|
|
206
|
+
test "neg"
|
|
207
|
+
r := -Vec2(x: 1.0, y: -2.0)
|
|
208
|
+
assert r.x == -1.0
|
|
209
|
+
assert r.y == 2.0
|
|
210
|
+
|
|
211
|
+
test "eq_and_neq"
|
|
212
|
+
a := Vec2(x: 1.0, y: 2.0)
|
|
213
|
+
assert a == Vec2(x: 1.0, y: 2.0)
|
|
214
|
+
assert a != Vec2(x: 1.0, y: 3.0)
|
|
215
|
+
assert !{a == Vec2(x: 0.0, y: 0.0)}
|
|
216
|
+
|
|
217
|
+
test "lt_ordering"
|
|
218
|
+
a := Vec2(x: 1.0, y: 0.0)
|
|
219
|
+
b := Vec2(x: 2.0, y: 0.0)
|
|
220
|
+
assert a < b
|
|
221
|
+
assert !{b < a}
|
|
222
|
+
assert a <= b
|
|
223
|
+
|
|
224
|
+
test "operators work through a generic"
|
|
225
|
+
d := double(Vec2(x: 1.0, y: 1.0))
|
|
226
|
+
assert d.x == 2.0
|
|
227
|
+
assert double(4) == 8
|
|
228
|
+
|
|
229
|
+
# ==== constants tests ====
|
|
230
|
+
|
|
231
|
+
test "consts are usable in fns, methods, generics and derived consts"
|
|
232
|
+
assert Grid.Height == 16
|
|
233
|
+
assert Grid.cells() == 128
|
|
234
|
+
assert Tile(x: 1).offset() == 101
|
|
235
|
+
assert Wrap(v: 3.5).area() == 128
|
|
236
|
+
assert scaled(2.0) == 5.0
|
|
237
|
+
assert Grid.Name == "plum"
|
|
238
|
+
assert Grid.DebugDraw
|
|
239
|
+
|
|
240
|
+
test "the same const name can live on two types"
|
|
241
|
+
assert Grid.Width == 8
|
|
242
|
+
assert Tile.Width == 100
|
|
243
|
+
|
|
244
|
+
test "a struct can hold a constant instance of itself"
|
|
245
|
+
assert ConstVec2.Zero.x == 0.0
|
|
246
|
+
assert Tile.origin().x == 0
|
|
247
|
+
|
|
248
|
+
test "a primitive holds a list constant and a static fn reading a sibling const"
|
|
249
|
+
assert Shelf.Names.length() == 3
|
|
250
|
+
assert Shelf.sidesTwice() == 8
|
plum-examples/operators.plum
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
module operators
|
|
2
|
-
import std/Str
|
|
3
|
-
import std/Int
|
|
4
|
-
import std/Float
|
|
5
|
-
|
|
6
|
-
# Operators on non-primitive operands call methods: `a + b` is `a.add(b)`,
|
|
7
|
-
# `-a` is `a.neg()`, `a == b` is `a.eq(b)`, `a < b` is `a.lt(b)`.
|
|
8
|
-
record Vec2(x: Float, y: Float) =
|
|
9
|
-
|
|
10
|
-
fun toStr(self) -> Str =
|
|
11
|
-
"Vec2({self.x}, {self.y})"
|
|
12
|
-
|
|
13
|
-
fun add(self, o: Vec2) -> Vec2 =
|
|
14
|
-
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
15
|
-
|
|
16
|
-
fun mul(self, s: Float) -> Vec2 =
|
|
17
|
-
Vec2(x: self.x * s, y: self.y * s)
|
|
18
|
-
|
|
19
|
-
fun neg(self) -> Vec2 =
|
|
20
|
-
Vec2(x: 0.0 - self.x, y: 0.0 - self.y)
|
|
21
|
-
|
|
22
|
-
fun eq(self, o: Vec2) -> Bool =
|
|
23
|
-
self.x == o.x && self.y == o.y
|
|
24
|
-
|
|
25
|
-
fun lt(self, o: Vec2) -> Bool =
|
|
26
|
-
self.x < o.x
|
|
27
|
-
|
|
28
|
-
fun le(self, o: Vec2) -> Bool =
|
|
29
|
-
self.x <= o.x
|
|
30
|
-
|
|
31
|
-
record Plain(v: Int)
|
|
32
|
-
|
|
33
|
-
fun double(a: T) -> T =
|
|
34
|
-
a + a
|
|
35
|
-
|
|
36
|
-
test "vec2_add_operator"
|
|
37
|
-
r := Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0)
|
|
38
|
-
assert r.x == 4.0
|
|
39
|
-
assert r.y == 6.0
|
|
40
|
-
|
|
41
|
-
test "mul_scalar_rhs"
|
|
42
|
-
r := Vec2(x: 1.0, y: 2.0) * 2.0
|
|
43
|
-
assert r.x == 2.0
|
|
44
|
-
assert r.y == 4.0
|
|
45
|
-
|
|
46
|
-
test "neg"
|
|
47
|
-
r := -Vec2(x: 1.0, y: -2.0)
|
|
48
|
-
assert r.x == -1.0
|
|
49
|
-
assert r.y == 2.0
|
|
50
|
-
|
|
51
|
-
test "eq_and_neq"
|
|
52
|
-
a := Vec2(x: 1.0, y: 2.0)
|
|
53
|
-
assert a == Vec2(x: 1.0, y: 2.0)
|
|
54
|
-
assert a != Vec2(x: 1.0, y: 3.0)
|
|
55
|
-
assert !{a == Vec2(x: 0.0, y: 0.0)}
|
|
56
|
-
|
|
57
|
-
test "lt_ordering"
|
|
58
|
-
a := Vec2(x: 1.0, y: 0.0)
|
|
59
|
-
b := Vec2(x: 2.0, y: 0.0)
|
|
60
|
-
assert a < b
|
|
61
|
-
assert !{b < a}
|
|
62
|
-
assert a <= b
|
|
63
|
-
|
|
64
|
-
test "operators work through a generic"
|
|
65
|
-
d := double(Vec2(x: 1.0, y: 1.0))
|
|
66
|
-
assert d.x == 2.0
|
|
67
|
-
assert double(4) == 8
|
plum-examples/optionals.plum
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import std/Int
|
|
2
|
-
import std/Str
|
|
3
|
-
import std/Slice
|
|
4
|
-
import std/Float
|
|
5
|
-
# Optionals: `T?` is a `T` or `null`. A plain `T` in a `T?` slot is the present
|
|
6
|
-
# value. An optional is handled with `?:` (Kotlin elvis), `if x := opt`,
|
|
7
|
-
# `while x := opt`, `opt!` (crashes on null) and `opt == null`; `catch` and `try`
|
|
8
|
-
# are for errors only. `!T?` is an error union whose success value is an optional.
|
|
9
|
-
#
|
|
10
|
-
# `obj?.field` / `obj?.method(args)` is safe navigation: it reaches into an
|
|
11
|
-
# optional without unwrapping it and passes null through. A field that is itself
|
|
12
|
-
# a `T?` flattens, so `u?.target?.name` is one `Str?`. It is sugar for
|
|
13
|
-
# `obj.andThen(|v| v.field)`.
|
|
14
|
-
|
|
15
|
-
record Box(label: Str)
|
|
16
|
-
|
|
17
|
-
record Holder(item: Box?, count: Int?)
|
|
18
|
-
|
|
19
|
-
fun maybeBox(c: Bool) -> Box? =
|
|
20
|
-
if c
|
|
21
|
-
return Box(label: "in")
|
|
22
|
-
return null
|
|
23
|
-
|
|
24
|
-
fun maybeInt(c: Bool) -> Int? =
|
|
25
|
-
if c
|
|
26
|
-
return 7
|
|
27
|
-
null
|
|
28
|
-
|
|
29
|
-
fun orFive(n: Int? = null) -> Int =
|
|
30
|
-
n ?: 5
|
|
31
|
-
|
|
32
|
-
fun failOnNull(c: Bool) -> error{Absent}!Int =
|
|
33
|
-
n := maybeInt(c) ?: return error.Absent
|
|
34
|
-
n + 1
|
|
35
|
-
|
|
36
|
-
fun nullOnNull(c: Bool) -> Int? =
|
|
37
|
-
n := maybeInt(c) ?: return null
|
|
38
|
-
n * 2
|
|
39
|
-
|
|
40
|
-
fun chainDefault(a: Int?, b: Int?) -> Int =
|
|
41
|
-
a ?: b ?: 99
|
|
42
|
-
|
|
43
|
-
fun fallible(n: Int) -> error{Bad}!Int? =
|
|
44
|
-
if n < 0
|
|
45
|
-
return error.Bad
|
|
46
|
-
if n == 0
|
|
47
|
-
return null
|
|
48
|
-
n
|
|
49
|
-
|
|
50
|
-
fun viaTry(n: Int) -> error{Bad}!Int =
|
|
51
|
-
v := try fallible(n)
|
|
52
|
-
v ?: -1
|
|
53
|
-
|
|
54
|
-
fun nextItem(items: Slice[Int], i: Int) -> Int? =
|
|
55
|
-
if i < items.length()
|
|
56
|
-
return items.get(i)!
|
|
57
|
-
null
|
|
58
|
-
|
|
59
|
-
fun sparse() -> Slice[Int?] =
|
|
60
|
-
Slice(1, null, 3)
|
|
61
|
-
|
|
62
|
-
fun sumBig() -> Int =
|
|
63
|
-
Int.Max + Int.Min
|
|
64
|
-
|
|
65
|
-
record Person(name: Str, age: Int) =
|
|
66
|
-
|
|
67
|
-
fun birthday(self) -> Int =
|
|
68
|
-
self.age + 1
|
|
69
|
-
|
|
70
|
-
record Target(name: Str)
|
|
71
|
-
|
|
72
|
-
record User(name: Str, target: Target?)
|
|
73
|
-
|
|
74
|
-
fun personName(p: Person?) -> Str? =
|
|
75
|
-
p?.name
|
|
76
|
-
|
|
77
|
-
fun personBirthday(p: Person?) -> Int? =
|
|
78
|
-
p?.birthday()
|
|
79
|
-
|
|
80
|
-
fun somePerson() -> Person? =
|
|
81
|
-
Person(name: "Ann", age: 3)
|
|
82
|
-
|
|
83
|
-
fun noPerson() -> Person? =
|
|
84
|
-
return null
|
|
85
|
-
|
|
86
|
-
fun noTarget() -> Target? =
|
|
87
|
-
return null
|
|
88
|
-
|
|
89
|
-
fun maybeCount(n: Int? = null) -> Int? =
|
|
90
|
-
n
|
|
91
|
-
|
|
92
|
-
fun noUser() -> User? =
|
|
93
|
-
return null
|
|
94
|
-
|
|
95
|
-
fun targetName(u: User?) -> Str? =
|
|
96
|
-
u?.target?.name
|
|
97
|
-
|
|
98
|
-
test "null_return_absent"
|
|
99
|
-
assert maybeBox(false) == null
|
|
100
|
-
assert maybeBox(true) != null
|
|
101
|
-
assert maybeInt(false) == null
|
|
102
|
-
assert null != maybeInt(true)
|
|
103
|
-
|
|
104
|
-
test "null_field_and_arg"
|
|
105
|
-
h := Holder(item: null, count: null)
|
|
106
|
-
assert h.item == null
|
|
107
|
-
assert h.count == null
|
|
108
|
-
h2 := Holder(item: Box(label: "x"), count: 3)
|
|
109
|
-
assert h2.count! == 3
|
|
110
|
-
assert orFive(null) == 5
|
|
111
|
-
assert orFive(8) == 8
|
|
112
|
-
xs := sparse()
|
|
113
|
-
assert xs.length() == 3
|
|
114
|
-
|
|
115
|
-
test "elvis_default"
|
|
116
|
-
assert {maybeInt(false) ?: 4} == 4
|
|
117
|
-
assert {maybeInt(true) ?: 4} == 7
|
|
118
|
-
assert {maybeBox(false)?.label ?: "none"} == "none"
|
|
119
|
-
assert {maybeBox(true)?.label ?: "none"} == "in"
|
|
120
|
-
|
|
121
|
-
test "elvis_return_error"
|
|
122
|
-
assert {failOnNull(true) catch 0} == 8
|
|
123
|
-
assert {failOnNull(false) catch 0} == 0
|
|
124
|
-
|
|
125
|
-
test "elvis_return_null"
|
|
126
|
-
assert nullOnNull(true)! == 14
|
|
127
|
-
assert nullOnNull(false) == null
|
|
128
|
-
|
|
129
|
-
test "elvis_chain_right_assoc"
|
|
130
|
-
assert chainDefault(null, null) == 99
|
|
131
|
-
assert chainDefault(null, 2) == 2
|
|
132
|
-
assert chainDefault(1, 2) == 1
|
|
133
|
-
|
|
134
|
-
test "if_bind_optional"
|
|
135
|
-
r := 0
|
|
136
|
-
if x := maybeInt(true)
|
|
137
|
-
r = x
|
|
138
|
-
else
|
|
139
|
-
r = -1
|
|
140
|
-
assert r == 7
|
|
141
|
-
if y := maybeInt(false)
|
|
142
|
-
r = y
|
|
143
|
-
else
|
|
144
|
-
r = -1
|
|
145
|
-
assert r == -1
|
|
146
|
-
assert {if v := maybeInt(true) then v + 1 else 0} == 8
|
|
147
|
-
assert {if w := maybeInt(false) then w + 1 else 0} == 0
|
|
148
|
-
if b := maybeBox(true)
|
|
149
|
-
assert b.label == "in"
|
|
150
|
-
|
|
151
|
-
test "while_bind_optional"
|
|
152
|
-
items := Slice(3, 2, 1)
|
|
153
|
-
total := 0
|
|
154
|
-
i := 0
|
|
155
|
-
while v := nextItem(items, i)
|
|
156
|
-
total = total + v
|
|
157
|
-
i = i + 1
|
|
158
|
-
assert total == 6
|
|
159
|
-
|
|
160
|
-
test "safe_nav_optional_flattens"
|
|
161
|
-
assert maybeBox(true)?.label! == "in"
|
|
162
|
-
assert maybeBox(false)?.label == null
|
|
163
|
-
|
|
164
|
-
test "null_eq_ref and null_eq_int"
|
|
165
|
-
b := maybeBox(false)
|
|
166
|
-
assert b == null
|
|
167
|
-
n := maybeInt(true)
|
|
168
|
-
assert n != null
|
|
169
|
-
assert !{n == null}
|
|
170
|
-
|
|
171
|
-
test "error_union_of_optional"
|
|
172
|
-
assert {viaTry(5) catch 0} == 5
|
|
173
|
-
assert {viaTry(0) catch 0} == -1
|
|
174
|
-
assert {viaTry(-3) catch 100} == 100
|
|
175
|
-
x := fallible(0)!
|
|
176
|
-
assert x == null
|
|
177
|
-
y := fallible(4)!
|
|
178
|
-
assert y! == 4
|
|
179
|
-
|
|
180
|
-
test "int_min_literal"
|
|
181
|
-
assert Int.Min == -0x8000_0000_0000_0000
|
|
182
|
-
assert sumBig() == -1
|
|
183
|
-
|
|
184
|
-
test "reassigning an optional local or field to null or a value"
|
|
185
|
-
o := maybeInt(true)
|
|
186
|
-
o = null
|
|
187
|
-
assert o == null
|
|
188
|
-
o = 4
|
|
189
|
-
assert o! == 4
|
|
190
|
-
b := maybeBox(true)
|
|
191
|
-
b = null
|
|
192
|
-
assert b == null
|
|
193
|
-
h := Holder(item: Box(label: "x"), count: 2)
|
|
194
|
-
h.count = null
|
|
195
|
-
h.item = null
|
|
196
|
-
assert h.count == null
|
|
197
|
-
assert h.item == null
|
|
198
|
-
h.count = 5
|
|
199
|
-
h.item = Box(label: "y")
|
|
200
|
-
assert h.count! == 5
|
|
201
|
-
assert h.item!.label == "y"
|
|
202
|
-
|
|
203
|
-
test "safe navigation maps a field access over a present value, passes absence through"
|
|
204
|
-
assert personName(somePerson())! == "Ann"
|
|
205
|
-
assert {personName(noPerson()) ?: "gone"} == "gone"
|
|
206
|
-
|
|
207
|
-
test "safe navigation maps a method call over a present value"
|
|
208
|
-
assert personBirthday(somePerson())! == 4
|
|
209
|
-
assert {personBirthday(noPerson()) ?: -1} == -1
|
|
210
|
-
|
|
211
|
-
test "safe navigation chains through a nullable field"
|
|
212
|
-
full := User(name: "u", target: Target(name: "home"))
|
|
213
|
-
assert {targetName(full) ?: "none"} == "home"
|
|
214
|
-
assert {targetName(User(name: "v", target: noTarget())) ?: "none"} == "none"
|
|
215
|
-
assert {targetName(noUser()) ?: "none"} == "none"
|
|
216
|
-
|
|
217
|
-
test "?: handles a nullable with a diverging or computed default"
|
|
218
|
-
p := somePerson() ?: crash "missing"
|
|
219
|
-
assert p.age == 3
|
|
220
|
-
q := noPerson() ?: Person(name: "gone", age: 1)
|
|
221
|
-
assert q.name == "gone"
|
|
222
|
-
|
|
223
|
-
test "null is the absent value in an argument, a list element and a default"
|
|
224
|
-
u := User(name: "w", target: null)
|
|
225
|
-
assert {targetName(u) ?: "none"} == "none"
|
|
226
|
-
xs := sparse()
|
|
227
|
-
second := xs.get(1)!
|
|
228
|
-
assert {second ?: 9} == 9
|
|
229
|
-
first := xs.get(0)!
|
|
230
|
-
assert first! == 1
|
|
231
|
-
assert {maybeCount() ?: 5} == 5
|
|
232
|
-
assert maybeCount(7)! == 7
|
plum-examples/own_list.plum
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import std/Int
|
|
2
|
-
import std/Float
|
|
3
|
-
import std/Slice
|
|
4
|
-
|
|
5
|
-
own record Cell(x: Float)
|
|
6
|
-
|
|
7
|
-
test "map_in_place_updates_all"
|
|
8
|
-
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
9
|
-
ys := xs.mapInPlace(|c| Cell(..c, x: c.x + 1.0))
|
|
10
|
-
assert ys.length() == 3
|
|
11
|
-
assert {ys.at(0).x - 2.0}.abs() < 1e-6
|
|
12
|
-
assert {ys.at(1).x - 3.0}.abs() < 1e-6
|
|
13
|
-
assert {ys.at(2).x - 4.0}.abs() < 1e-6
|
|
14
|
-
|
|
15
|
-
test "take_then_put_round_trip"
|
|
16
|
-
xs := Slice(Cell(x: 1.0), Cell(x: 2.0))
|
|
17
|
-
c := xs.takeAt(1)
|
|
18
|
-
d := Cell(..c, x: c.x + 10.0)
|
|
19
|
-
xs.putAt(1, d)
|
|
20
|
-
assert xs.length() == 2
|
|
21
|
-
assert {xs.at(1).x - 12.0}.abs() < 1e-6
|
|
22
|
-
assert {xs.at(0).x - 1.0}.abs() < 1e-6
|
|
23
|
-
|
|
24
|
-
test "at_borrows"
|
|
25
|
-
xs := Slice(Cell(x: 5.0))
|
|
26
|
-
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
27
|
-
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
28
|
-
|
|
29
|
-
test "put_after_take_keeps_length"
|
|
30
|
-
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
31
|
-
c := xs.takeAt(0)
|
|
32
|
-
xs.putAt(0, c)
|
|
33
|
-
assert xs.length() == 3
|
plum-examples/recursion.plum
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Recursion: tail calls, the cons list and in-place slice updates.
|
|
2
|
+
import std/Int
|
|
3
|
+
import std/Float
|
|
4
|
+
import std/List
|
|
5
|
+
import std/Str
|
|
6
|
+
import std/Slice
|
|
7
|
+
import std/Array
|
|
8
|
+
|
|
9
|
+
# ==== tail calls ====
|
|
10
|
+
|
|
11
|
+
# Tail calls: deep recursion that must not overflow the stack.
|
|
12
|
+
#
|
|
13
|
+
# Self tail calls compile to loops, other tail calls use `return_call`, and a
|
|
14
|
+
# constructor wrapped self call (`Cons(head: f(h), tail: map(t, f))`) is rewritten
|
|
15
|
+
# to a loop (tail recursion modulo cons). All deep tests use 1,000,000 steps.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
fun sumTo(n: Int, acc: Int) -> Int =
|
|
19
|
+
if n == 0 then acc else sumTo(n - 1, acc + n)
|
|
20
|
+
|
|
21
|
+
fun build(n: Int, acc: List[Int]) -> List[Int] =
|
|
22
|
+
if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
|
|
23
|
+
|
|
24
|
+
fun fromTo(a: Int, b: Int) -> List[Int] =
|
|
25
|
+
if a >= b then Nil else Cons(head: a, tail: fromTo(a + 1, b))
|
|
26
|
+
|
|
27
|
+
fun map(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
28
|
+
Nil => Nil
|
|
29
|
+
Cons(h, t) => Cons(head: f(h), tail: map(t, f))
|
|
30
|
+
|
|
31
|
+
fun foldl(xs: List[T], acc: A, f: fn(A, T) -> A) -> A = match xs
|
|
32
|
+
Nil => acc
|
|
33
|
+
Cons(h, t) => foldl(t, f(acc, h), f)
|
|
34
|
+
|
|
35
|
+
fun length(xs: List[T], acc: Int) -> Int = match xs
|
|
36
|
+
Nil => acc
|
|
37
|
+
Cons(_, t) => length(t, acc + 1)
|
|
38
|
+
|
|
39
|
+
fun isEven(n: Int) -> Bool =
|
|
40
|
+
if n == 0 then true else isOdd(n - 1)
|
|
41
|
+
|
|
42
|
+
fun isOdd(n: Int) -> Bool =
|
|
43
|
+
if n == 0 then false else isEven(n - 1)
|
|
44
|
+
|
|
45
|
+
# `call` tail calls a closure value; `dec` tail calls `call`.
|
|
46
|
+
fun call(f: fn(Int) -> Int, n: Int) -> Int = f(n)
|
|
47
|
+
|
|
48
|
+
fun dec(n: Int) -> Int =
|
|
49
|
+
if n == 0 then 0 else call(dec, n - 1)
|
|
50
|
+
|
|
51
|
+
record Counter(n: Int, total: Int)
|
|
52
|
+
|
|
53
|
+
fun spin(c: Counter) -> Counter =
|
|
54
|
+
if c.n == 0 then c else spin(Counter(..c, n: c.n - 1, total: c.total + 2))
|
|
55
|
+
|
|
56
|
+
fun depth(n: Int) -> Int =
|
|
57
|
+
if n == 0 then 0 else 1 + depth(n - 1)
|
|
58
|
+
|
|
59
|
+
fun down(n: Int) -> error{Neg}!Int =
|
|
60
|
+
if n < 0
|
|
61
|
+
return error.Neg
|
|
62
|
+
if n == 0
|
|
63
|
+
return 5
|
|
64
|
+
down(n - 1)
|
|
65
|
+
|
|
66
|
+
# ==== cons list ====
|
|
67
|
+
|
|
68
|
+
# The std cons List: constant stack at 1,000,000 elements.
|
|
69
|
+
|
|
70
|
+
fun range(a: Int, b: Int) -> List[Int] =
|
|
71
|
+
if a >= b then Nil else Cons(head: a, tail: range(a + 1, b))
|
|
72
|
+
|
|
73
|
+
# ==== own list ====
|
|
74
|
+
|
|
75
|
+
record Cell(x: Float)
|
|
76
|
+
|
|
77
|
+
# ==== tail calls tests ====
|
|
78
|
+
|
|
79
|
+
test "tail_sum_million"
|
|
80
|
+
assert sumTo(1000000, 0) == 500000500000
|
|
81
|
+
|
|
82
|
+
test "tail_build_million"
|
|
83
|
+
assert length(build(1000000, Nil), 0) == 1000000
|
|
84
|
+
|
|
85
|
+
test "fold_million"
|
|
86
|
+
xs := fromTo(0, 1000000)
|
|
87
|
+
assert foldl(xs, 0, |a, x| a + x) == 499999500000
|
|
88
|
+
|
|
89
|
+
test "mutual_even_odd_million"
|
|
90
|
+
assert isEven(1000000)
|
|
91
|
+
assert !isOdd(1000000)
|
|
92
|
+
assert isOdd(999999)
|
|
93
|
+
|
|
94
|
+
test "trmc_map_million"
|
|
95
|
+
ys := map(fromTo(0, 1000000), |x| x + 1)
|
|
96
|
+
assert length(ys, 0) == 1000000
|
|
97
|
+
assert foldl(ys, 0, |a, x| a + x) == 500000500000
|
|
98
|
+
|
|
99
|
+
test "trmc_fromTo_million"
|
|
100
|
+
xs := fromTo(0, 1000000)
|
|
101
|
+
assert length(xs, 0) == 1000000
|
|
102
|
+
|
|
103
|
+
test "closure_tail_million"
|
|
104
|
+
assert dec(1000000) == 0
|
|
105
|
+
|
|
106
|
+
test "own_record_loop_100k"
|
|
107
|
+
c := spin(Counter(n: 100000, total: 0))
|
|
108
|
+
assert c.n == 0
|
|
109
|
+
assert c.total == 200000
|
|
110
|
+
|
|
111
|
+
test "recursive_closure_million"
|
|
112
|
+
go := |n, acc| if n == 0 then acc else go(n - 1, acc + n)
|
|
113
|
+
assert go(1000000, 0) == 500000500000
|
|
114
|
+
|
|
115
|
+
test "closure_body_tail_million"
|
|
116
|
+
fin := |n: Int| sumTo(n, 0)
|
|
117
|
+
assert fin(1000000) == 500000500000
|
|
118
|
+
|
|
119
|
+
test "error_union_loop_million"
|
|
120
|
+
assert { down(1000000) catch 99 } == 5
|
|
121
|
+
|
|
122
|
+
test "map_single_pass_million"
|
|
123
|
+
ys := map(fromTo(0, 1000000), |x| x * 2)
|
|
124
|
+
assert foldl(ys, 0, |a, x| a + x) == 999999000000
|
|
125
|
+
|
|
126
|
+
# Non-tail recursion still uses the wasm stack. Measured limit under the 4 MiB
|
|
127
|
+
# host stack: about 265,000 frames (260,000 passes, 270,000 traps) of `1 + depth(n - 1)`; tested well below it.
|
|
128
|
+
test "tree_recursion_depth_recorded"
|
|
129
|
+
assert depth(100000) == 100000
|
|
130
|
+
|
|
131
|
+
# ==== cons list tests ====
|
|
132
|
+
|
|
133
|
+
test "init_sum"
|
|
134
|
+
assert List(1, 2, 3, 4).fold(0, |a, x| a + x) == 10
|
|
135
|
+
assert List(1, 2, 3).length() == 3
|
|
136
|
+
assert { List(1, 2, 3).get(1) ?: -1 } == 2
|
|
137
|
+
assert List(1, 2, 3).get(5) == null
|
|
138
|
+
nil: List[Int] := Nil
|
|
139
|
+
assert nil.isEmpty()
|
|
140
|
+
assert !List(1).isEmpty()
|
|
141
|
+
|
|
142
|
+
test "map_filter_fold"
|
|
143
|
+
xs := List(1, 2, 3, 4, 5, 6)
|
|
144
|
+
ys := xs.map(|x| x * 2).filter(|x| x % 3 == 0)
|
|
145
|
+
assert ys.length() == 2
|
|
146
|
+
assert ys.fold(0, |a, x| a + x) == 18
|
|
147
|
+
|
|
148
|
+
test "reverse_append"
|
|
149
|
+
xs := List(1, 2, 3).reverse()
|
|
150
|
+
assert { xs.get(0) ?: -1 } == 3
|
|
151
|
+
assert { xs.get(2) ?: -1 } == 1
|
|
152
|
+
ys := List(1, 2).append(List(3, 4))
|
|
153
|
+
assert ys.equals(List(1, 2, 3, 4))
|
|
154
|
+
|
|
155
|
+
test "for_range"
|
|
156
|
+
total := 0
|
|
157
|
+
for x := range List(1, 2, 3, 4)
|
|
158
|
+
total = total + x
|
|
159
|
+
assert total == 10
|
|
160
|
+
big := 0
|
|
161
|
+
for x := range range(0, 1000000).items()
|
|
162
|
+
big = big + x
|
|
163
|
+
assert big == 499999500000
|
|
164
|
+
|
|
165
|
+
test "to_slice_round_trip"
|
|
166
|
+
s := List(1, 2, 3).map(|x| x * 2).toSlice()
|
|
167
|
+
assert s.equals(Slice(2, 4, 6))
|
|
168
|
+
assert List.fromSlice(s).equals(List(2, 4, 6))
|
|
169
|
+
|
|
170
|
+
test "to_array"
|
|
171
|
+
a := List(7, 8, 9).toArray()
|
|
172
|
+
assert a.length() == 3
|
|
173
|
+
assert a.get(2) == 9
|
|
174
|
+
|
|
175
|
+
test "map_million"
|
|
176
|
+
ys := range(0, 1000000).map(|x| x + 1)
|
|
177
|
+
assert ys.length() == 1000000
|
|
178
|
+
assert ys.fold(0, |a, x| a + x) == 500000500000
|
|
179
|
+
|
|
180
|
+
test "cons_fold_million"
|
|
181
|
+
xs := range(0, 1000000)
|
|
182
|
+
assert xs.fold(0, |a, x| a + x) == 499999500000
|
|
183
|
+
assert xs.filter(|x| x % 2 == 0).length() == 500000
|
|
184
|
+
assert { xs.reverse().get(0) ?: -1 } == 999999
|
|
185
|
+
|
|
186
|
+
test "equals_and_toStr"
|
|
187
|
+
assert List(1, 2, 3).equals(List(1, 2, 3))
|
|
188
|
+
assert !List(1, 2, 3).equals(List(1, 2))
|
|
189
|
+
assert !List(1, 2).equals(List(1, 2, 3))
|
|
190
|
+
assert !List(1, 2, 3).equals(List(1, 2, 4))
|
|
191
|
+
assert List(1, 2, 3).toStr() == "[1,2,3]"
|
|
192
|
+
|
|
193
|
+
# ==== own list tests ====
|
|
194
|
+
|
|
195
|
+
test "map_in_place_updates_all"
|
|
196
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
197
|
+
ys := xs.mapInPlace(|c| Cell(..c, x: c.x + 1.0))
|
|
198
|
+
assert ys.length() == 3
|
|
199
|
+
assert {ys.at(0).x - 2.0}.abs() < 1e-6
|
|
200
|
+
assert {ys.at(1).x - 3.0}.abs() < 1e-6
|
|
201
|
+
assert {ys.at(2).x - 4.0}.abs() < 1e-6
|
|
202
|
+
|
|
203
|
+
test "take_then_put_round_trip"
|
|
204
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0))
|
|
205
|
+
c := xs.takeAt(1)
|
|
206
|
+
d := Cell(..c, x: c.x + 10.0)
|
|
207
|
+
xs.putAt(1, d)
|
|
208
|
+
assert xs.length() == 2
|
|
209
|
+
assert {xs.at(1).x - 12.0}.abs() < 1e-6
|
|
210
|
+
assert {xs.at(0).x - 1.0}.abs() < 1e-6
|
|
211
|
+
|
|
212
|
+
test "at_borrows"
|
|
213
|
+
xs := Slice(Cell(x: 5.0))
|
|
214
|
+
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
215
|
+
assert {xs.at(0).x - 5.0}.abs() < 1e-6
|
|
216
|
+
|
|
217
|
+
test "put_after_take_keeps_length"
|
|
218
|
+
xs := Slice(Cell(x: 1.0), Cell(x: 2.0), Cell(x: 3.0))
|
|
219
|
+
c := xs.takeAt(0)
|
|
220
|
+
xs.putAt(0, c)
|
|
221
|
+
assert xs.length() == 3
|
plum-examples/{seq.plum → sequences.plum}
RENAMED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sequences: Seq adapters and terminals, and user-defined fun* forms.
|
|
2
|
-
# `skip`, `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
|
|
3
2
|
import std/Int
|
|
4
3
|
import std/Float
|
|
5
4
|
import std/Slice
|
|
6
5
|
import std/Seq
|
|
7
6
|
import std/Map
|
|
8
7
|
import std/Str
|
|
8
|
+
import std/Testing
|
|
9
|
+
|
|
10
|
+
# ==== seq ====
|
|
11
|
+
|
|
12
|
+
# Sequence adapters, terminals and chains: using `std/Seq` methods (`take`,
|
|
13
|
+
# `skip`, `map`, `filter`, `find`, `fold`) to process collections in `for` loops.
|
|
9
14
|
|
|
10
15
|
# Seq methods (`extend Seq[T]` in plum-std/Seq.plum): `xs.filter()` & co. in a
|
|
11
16
|
# `for` source. Adapters (take, skip, stepBy, zip, reversed, chunks) reshape the
|
|
@@ -47,6 +52,78 @@ fun army() -> Slice[Soldier] =
|
|
|
47
52
|
Soldier(name: "f", team: 2, hp: 8.0, x: 1.0, y: 0.0),
|
|
48
53
|
)
|
|
49
54
|
|
|
55
|
+
# ==== forms ====
|
|
56
|
+
|
|
57
|
+
# User-defined `fun*` forms with `yield`: sources, folds and methods written in
|
|
58
|
+
# plum and expanded at the call site (the built-in forms are in seq.plum).
|
|
59
|
+
|
|
60
|
+
# for-forms: `for binders := range form(args), clauses` + body. A form is a `fun*`
|
|
61
|
+
# with a yield type (`*T`); each `yield` runs the `for` body, and the whole
|
|
62
|
+
# thing is expanded at the call site, so there are no closures. See
|
|
63
|
+
# plum-std/Seq.plum and plum-core/src/forms.rs.
|
|
64
|
+
|
|
65
|
+
record Fighter(name: Str, hp: Int, dead: Bool)
|
|
66
|
+
|
|
67
|
+
fun sampleFighters() -> Slice[Fighter] =
|
|
68
|
+
Slice(Fighter(name: "a", hp: 10, dead: false), Fighter(name: "b", hp: 0, dead: true), Fighter(name: "c", hp: 7, dead: false))
|
|
69
|
+
|
|
70
|
+
# a user form: the sum of the body's value over a list
|
|
71
|
+
fun* sumBy(xs: Slice[T]) *T -> Int =
|
|
72
|
+
total := 0
|
|
73
|
+
for x := range xs
|
|
74
|
+
v := yield x
|
|
75
|
+
total = total + v
|
|
76
|
+
total
|
|
77
|
+
|
|
78
|
+
# a user form with two binders and a statement body: the 8 neighbours of (x, y)
|
|
79
|
+
fun* neighbours(x: Int, y: Int) *(Int, Int) =
|
|
80
|
+
for dx := range 3
|
|
81
|
+
for dy := range 3
|
|
82
|
+
if dx != 1 || dy != 1
|
|
83
|
+
yield x + dx - 1, y + dy - 1
|
|
84
|
+
|
|
85
|
+
# a user form built from another form: `yield` sits inside `each`'s body
|
|
86
|
+
fun* sumEvens(xs: Slice[Int]) *Int -> Int =
|
|
87
|
+
total := 0
|
|
88
|
+
for x := range xs.each(), if x % 2 == 0
|
|
89
|
+
v := yield x
|
|
90
|
+
total = total + v
|
|
91
|
+
total
|
|
92
|
+
|
|
93
|
+
fun firstBig(xs: Slice[Int]) -> Int =
|
|
94
|
+
for x := range xs.each()
|
|
95
|
+
if x > 10
|
|
96
|
+
return x
|
|
97
|
+
return -1
|
|
98
|
+
|
|
99
|
+
fun returnedForm(xs: Slice[Int]) -> Slice[Int] =
|
|
100
|
+
return for x := range xs.map()
|
|
101
|
+
x + 1
|
|
102
|
+
|
|
103
|
+
fun tailForm(xs: Slice[Int]) -> Int =
|
|
104
|
+
for x := range sumBy(xs)
|
|
105
|
+
x * 2
|
|
106
|
+
|
|
107
|
+
# A type with a fun* method: `self` is bound to the receiver at the use site.
|
|
108
|
+
record Countdown(from: Int) =
|
|
109
|
+
|
|
110
|
+
fun* ticks(self) *Int =
|
|
111
|
+
i := self.from
|
|
112
|
+
while i > 0
|
|
113
|
+
cur := i
|
|
114
|
+
i = i - 1
|
|
115
|
+
yield cur
|
|
116
|
+
|
|
117
|
+
# `for a, b := range countdown`: two binders pick `entries`.
|
|
118
|
+
fun* entries(self) *(Int, Int) =
|
|
119
|
+
i := self.from
|
|
120
|
+
while i > 0
|
|
121
|
+
cur := i
|
|
122
|
+
i = i - 1
|
|
123
|
+
yield cur, cur * cur
|
|
124
|
+
|
|
125
|
+
# ==== seq tests ====
|
|
126
|
+
|
|
50
127
|
test "seq_method_filter: a List has no filter of its own, so Seq.filter is used"
|
|
51
128
|
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
52
129
|
evens := for x := range xs.filter()
|
|
@@ -234,3 +311,85 @@ test "materializing stages are separate statements"
|
|
|
234
311
|
hps := for u := range alive.map(), if u.team == 1
|
|
235
312
|
u.hp
|
|
236
313
|
assert hps.length() == 3
|
|
314
|
+
|
|
315
|
+
# ==== forms tests ====
|
|
316
|
+
|
|
317
|
+
test "user_form_sumBy"
|
|
318
|
+
fighters := sampleFighters()
|
|
319
|
+
hp := for u := range sumBy(fighters)
|
|
320
|
+
u.hp
|
|
321
|
+
assert hp == 17
|
|
322
|
+
odd := for x := range sumBy(Slice(1, 2, 3, 4, 5)), if x % 2 == 1
|
|
323
|
+
x
|
|
324
|
+
assert odd == 9
|
|
325
|
+
|
|
326
|
+
test "user_form_neighbours_grid: two binders, statement body"
|
|
327
|
+
count := 0
|
|
328
|
+
sum := 0
|
|
329
|
+
for nx, ny := range neighbours(5, 5), if nx > 5
|
|
330
|
+
count = count + 1
|
|
331
|
+
sum = sum + ny
|
|
332
|
+
assert count == 3
|
|
333
|
+
assert sum == 15
|
|
334
|
+
|
|
335
|
+
test "break_continue_return_semantics"
|
|
336
|
+
xs := Slice(1, 2, 3, 4, 5)
|
|
337
|
+
total := 0
|
|
338
|
+
for x := range xs.each()
|
|
339
|
+
if x == 2
|
|
340
|
+
continue
|
|
341
|
+
if x == 5
|
|
342
|
+
break
|
|
343
|
+
total = total + x
|
|
344
|
+
assert total == 8
|
|
345
|
+
assert firstBig(Slice(1, 20, 30)) == 20
|
|
346
|
+
assert firstBig(xs) == -1
|
|
347
|
+
prefix := for x := range xs.filter()
|
|
348
|
+
if x == 4
|
|
349
|
+
break
|
|
350
|
+
x > 1
|
|
351
|
+
assert prefix.length() == 2
|
|
352
|
+
|
|
353
|
+
test "composed form: a form that uses another form"
|
|
354
|
+
xs := Slice(1, 2, 3, 4, 5, 6)
|
|
355
|
+
total := for x := range sumEvens(xs)
|
|
356
|
+
x * 10
|
|
357
|
+
assert total == 120
|
|
358
|
+
|
|
359
|
+
test "forms nest: a form inside a form body"
|
|
360
|
+
xs := Slice(1, 2)
|
|
361
|
+
ys := Slice(10, 20)
|
|
362
|
+
total := for a := range sumBy(xs)
|
|
363
|
+
inner := for b := range sumBy(ys)
|
|
364
|
+
a * b
|
|
365
|
+
inner
|
|
366
|
+
assert total == 90
|
|
367
|
+
|
|
368
|
+
test "return for-form: the form's value is returned"
|
|
369
|
+
ys := returnedForm(Slice(1, 2, 3))
|
|
370
|
+
assert ys.length() == 3
|
|
371
|
+
assert ys.at(2) == 4
|
|
372
|
+
|
|
373
|
+
test "tail for-form: the last statement of a fn is its value"
|
|
374
|
+
assert tailForm(Slice(1, 2, 3)) == 12
|
|
375
|
+
|
|
376
|
+
test "fun_star_method_uses_self"
|
|
377
|
+
c := Countdown(from: 4)
|
|
378
|
+
seen := 0
|
|
379
|
+
for t := range c.ticks()
|
|
380
|
+
seen = seen * 10 + t
|
|
381
|
+
assert seen == 4321
|
|
382
|
+
evens := for t := range c.ticks().map(), if t % 2 == 0
|
|
383
|
+
t * 100
|
|
384
|
+
assert evens.length() == 2
|
|
385
|
+
assert evens.at(0) == 400
|
|
386
|
+
|
|
387
|
+
test "two_binder_uses_entries_method: a custom type's entries()"
|
|
388
|
+
c := Countdown(from: 3)
|
|
389
|
+
total := 0
|
|
390
|
+
for n, sq := range c
|
|
391
|
+
total = total + n * 1000 + sq
|
|
392
|
+
assert total == 3 * 1000 + 9 + 2 * 1000 + 4 + 1000 + 1
|
|
393
|
+
for _, sq := range c
|
|
394
|
+
total = total + sq
|
|
395
|
+
assert total == 6014 + 14
|
plum-examples/struct_union_members.plum
CHANGED
|
@@ -16,7 +16,7 @@ struct Wait(left: Float)
|
|
|
16
16
|
|
|
17
17
|
record Chain(steps: Slice[Step])
|
|
18
18
|
|
|
19
|
-
trait Thing(x: Float, tag: Int) =
|
|
19
|
+
trait Thing(mut x: Float, tag: Int) =
|
|
20
20
|
fun clone(self) -> Thing
|
|
21
21
|
|
|
22
22
|
record Box(Thing(x, tag), size: Float) =
|
plum-examples/structs_records.plum
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
# Structs and records
|
|
1
|
+
# Structs and records, digit names and annotated locals.
|
|
2
|
-
# into wasm locals, no heap allocation); `record` is a reference type shared on
|
|
3
|
-
# assignment. Both have named fields, methods, constants, positional or named
|
|
4
|
-
# construction, field mutation and spread update (`Point(..p, x: 1)`).
|
|
5
|
-
module structs_records
|
|
6
2
|
import std/Str
|
|
7
3
|
import std/Int
|
|
8
4
|
import std/Float
|
|
9
5
|
import std/Map
|
|
6
|
+
import std/Slice
|
|
7
|
+
|
|
8
|
+
# ==== structs records ====
|
|
9
|
+
|
|
10
|
+
# Structs and records: `struct` is a small copy-semantics value type (flattened
|
|
11
|
+
# into wasm locals, no heap allocation); `record` is a reference type shared on
|
|
12
|
+
# assignment. Both have named fields, methods, constants, positional or named
|
|
13
|
+
# construction, field mutation and spread update (`Point(..p, x: 1)`).
|
|
10
14
|
|
|
11
15
|
# ---- struct: value semantics ----
|
|
12
16
|
|
|
@@ -36,7 +40,7 @@ struct Color(r: Int, g: Int, b: Int, a: Int)
|
|
|
36
40
|
|
|
37
41
|
struct Cell(pos: Vec2, solid: Bool, tint: Color)
|
|
38
42
|
|
|
39
|
-
record Player(pos: Vec2, hp: Int)
|
|
43
|
+
record Player(mut pos: Vec2, hp: Int)
|
|
40
44
|
|
|
41
45
|
fun applyTo(v: Vec2, cb: fn(Vec2) -> Vec2) -> Vec2 =
|
|
42
46
|
cb(v)
|
|
@@ -81,7 +85,7 @@ fun bump(v: Vec2) -> Float =
|
|
|
81
85
|
|
|
82
86
|
# ---- record: reference semantics ----
|
|
83
87
|
|
|
84
|
-
record Point(x: Int, y: Int)
|
|
88
|
+
record Point(mut x: Int, y: Int)
|
|
85
89
|
|
|
86
90
|
# A method takes `self` first; a `fun` without `self` is a static (`Vec2.zero()`).
|
|
87
91
|
record Cat(name: Str, age: Int) =
|
|
@@ -123,10 +127,9 @@ record Bag(n: Int) =
|
|
|
123
127
|
|
|
124
128
|
# ---- embedding records and override ----
|
|
125
129
|
|
|
126
|
-
record Counter(
|
|
130
|
+
record Counter(mut count: Int) =
|
|
127
131
|
|
|
128
132
|
fun init(self) =
|
|
129
|
-
self.count: Int = self.start
|
|
130
133
|
self.log: Str = ""
|
|
131
134
|
|
|
132
135
|
fun bump(self) =
|
|
@@ -135,7 +138,7 @@ record Counter(start: Int) =
|
|
|
135
138
|
fun show(self) -> Str =
|
|
136
139
|
"counter {self.count}"
|
|
137
140
|
|
|
138
|
-
record Scoreboard(Counter(
|
|
141
|
+
record Scoreboard(Counter(count), name: Str) =
|
|
139
142
|
|
|
140
143
|
fun init(self) =
|
|
141
144
|
self.best: Int = 0
|
|
@@ -143,6 +146,76 @@ record Scoreboard(Counter(start), name: Str) =
|
|
|
143
146
|
override fun show(self) -> Str =
|
|
144
147
|
"{self.name}: {self.Counter.show()}"
|
|
145
148
|
|
|
149
|
+
# ==== digit names ====
|
|
150
|
+
|
|
151
|
+
# Type and variant names may contain digits (`V2`, `A1`, `E1`). They are
|
|
152
|
+
# resolved by position: where a type or variant name is expected it is one;
|
|
153
|
+
# `Type.Name` consts such as `V2.K1` and `Int.Max` keep working.
|
|
154
|
+
|
|
155
|
+
enum V2 =
|
|
156
|
+
| A1
|
|
157
|
+
| B2
|
|
158
|
+
K1 = 40
|
|
159
|
+
|
|
160
|
+
record A1(n: Int)
|
|
161
|
+
|
|
162
|
+
error S1 = E1 | E2
|
|
163
|
+
|
|
164
|
+
fun fail(n: Int) -> S1!Int =
|
|
165
|
+
if n == 1
|
|
166
|
+
return error.E1
|
|
167
|
+
if n == 2
|
|
168
|
+
return error.E2
|
|
169
|
+
n
|
|
170
|
+
|
|
171
|
+
fun asInt(w: V2) -> Int =
|
|
172
|
+
match w
|
|
173
|
+
A1(n) => n
|
|
174
|
+
B2 => 0
|
|
175
|
+
|
|
176
|
+
fun payload(w: V2) -> Int =
|
|
177
|
+
if A1(n) := w
|
|
178
|
+
return n
|
|
179
|
+
0
|
|
180
|
+
|
|
181
|
+
fun isB2(w: V2) -> Int =
|
|
182
|
+
if B2 := w
|
|
183
|
+
return 1
|
|
184
|
+
0
|
|
185
|
+
|
|
186
|
+
# ==== annotated locals ====
|
|
187
|
+
|
|
188
|
+
# Annotated local declarations: `name: Type := value` fixes the local's type to
|
|
189
|
+
# the annotation. The value converts to it exactly as it would for a parameter of
|
|
190
|
+
# that type (a union member into its enum, `T` into `T?`, `null` into `T?`), and
|
|
191
|
+
# a later `name = value` is checked and converted against the same type.
|
|
192
|
+
|
|
193
|
+
record Circle(r: Float)
|
|
194
|
+
|
|
195
|
+
record Region(w: Float, h: Float)
|
|
196
|
+
|
|
197
|
+
enum Shape = Circle | Region
|
|
198
|
+
|
|
199
|
+
error LocalError = Bad | Worse
|
|
200
|
+
|
|
201
|
+
fun area(s: Shape) -> Float =
|
|
202
|
+
match s
|
|
203
|
+
Circle(r) => r * r * 3.0
|
|
204
|
+
Region(w, h) => w * h
|
|
205
|
+
|
|
206
|
+
fun parseLocal(n: Int) -> LocalError!Int =
|
|
207
|
+
if n < 0
|
|
208
|
+
return error.Bad
|
|
209
|
+
n * 2
|
|
210
|
+
|
|
211
|
+
fun twice(x: Int) -> Int = x * 2
|
|
212
|
+
|
|
213
|
+
fun same(x: T) -> T =
|
|
214
|
+
y: T := x
|
|
215
|
+
y
|
|
216
|
+
|
|
217
|
+
# ==== structs records tests ====
|
|
218
|
+
|
|
146
219
|
test "struct assignment copies"
|
|
147
220
|
a := Vec2(x: 1.0, y: 2.0)
|
|
148
221
|
b := a
|
|
@@ -299,12 +372,83 @@ test "init lists are fresh per construction"
|
|
|
299
372
|
assert b.items.length() == 0
|
|
300
373
|
|
|
301
374
|
test "a record embeds another: fields, init and methods are copied"
|
|
302
|
-
s := Scoreboard(
|
|
375
|
+
s := Scoreboard(count: 5, name: "hi")
|
|
303
376
|
assert s.count == 5
|
|
304
377
|
s.bump()
|
|
305
378
|
assert s.count == 6
|
|
306
379
|
assert s.best == 0
|
|
307
380
|
|
|
308
381
|
test "override replaces an embedded method and self.X.m() calls the original"
|
|
309
|
-
s := Scoreboard(
|
|
382
|
+
s := Scoreboard(count: 1, name: "hi")
|
|
310
383
|
assert s.show() == "hi: counter 1"
|
|
384
|
+
|
|
385
|
+
# ==== digit names tests ====
|
|
386
|
+
|
|
387
|
+
test "digit_variant_match"
|
|
388
|
+
assert asInt(A1(n: 5)) == 5
|
|
389
|
+
x := B2
|
|
390
|
+
assert asInt(x) == 0
|
|
391
|
+
|
|
392
|
+
test "digit_variant_pattern_if"
|
|
393
|
+
assert payload(A1(n: 7)) == 7
|
|
394
|
+
assert payload(B2) == 0
|
|
395
|
+
w := B2
|
|
396
|
+
assert isB2(w) == 1
|
|
397
|
+
assert isB2(A1(n: 1)) == 0
|
|
398
|
+
|
|
399
|
+
test "digit_error_names"
|
|
400
|
+
a := fail(3) catch 0
|
|
401
|
+
assert a == 3
|
|
402
|
+
b := fail(1) catch 9
|
|
403
|
+
assert b == 9
|
|
404
|
+
|
|
405
|
+
test "digit_enum_const_still_works"
|
|
406
|
+
assert V2.K1 == 40
|
|
407
|
+
assert Int.Max > 0
|
|
408
|
+
|
|
409
|
+
# ==== annotated locals tests ====
|
|
410
|
+
|
|
411
|
+
test "union_local"
|
|
412
|
+
s: Shape := Circle(r: 2.0)
|
|
413
|
+
assert area(s) == 12.0
|
|
414
|
+
|
|
415
|
+
test "union_local_reassigned"
|
|
416
|
+
s: Shape := Circle(r: 1.0)
|
|
417
|
+
s = Region(w: 2.0, h: 3.0)
|
|
418
|
+
assert area(s) == 6.0
|
|
419
|
+
|
|
420
|
+
test "optional_local_present"
|
|
421
|
+
o: Int? := 4
|
|
422
|
+
assert {o ?: 0} == 4
|
|
423
|
+
|
|
424
|
+
test "optional_local_null"
|
|
425
|
+
o: Int? := null
|
|
426
|
+
assert {o ?: 9} == 9
|
|
427
|
+
o = 5
|
|
428
|
+
assert {o ?: 9} == 5
|
|
429
|
+
|
|
430
|
+
test "list_local"
|
|
431
|
+
xs: Slice[Shape] := Slice[Shape]()
|
|
432
|
+
xs.push(Circle(r: 1.0))
|
|
433
|
+
xs.push(Region(w: 1.0, h: 1.0))
|
|
434
|
+
assert xs.length() == 2
|
|
435
|
+
|
|
436
|
+
test "fn_local"
|
|
437
|
+
f: fn(Int) -> Int := twice
|
|
438
|
+
assert f(4) == 8
|
|
439
|
+
|
|
440
|
+
test "scalar_locals"
|
|
441
|
+
n: Int := 3
|
|
442
|
+
x: Float := 2.5
|
|
443
|
+
assert n + 1 == 4
|
|
444
|
+
assert x * 2.0 == 5.0
|
|
445
|
+
|
|
446
|
+
test "generic_param_annotation"
|
|
447
|
+
assert same(7) == 7
|
|
448
|
+
assert same("a") == "a"
|
|
449
|
+
|
|
450
|
+
test "named_error_set_local"
|
|
451
|
+
ok: LocalError!Int := parseLocal(4)
|
|
452
|
+
bad: LocalError!Int := parseLocal(-1)
|
|
453
|
+
assert {ok catch 0} == 8
|
|
454
|
+
assert {bad catch 7} == 7
|
plum-examples/tail_calls.plum
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Tail calls: deep recursion that must not overflow the stack.
|
|
2
|
-
#
|
|
3
|
-
# Self tail calls compile to loops, other tail calls use `return_call`, and a
|
|
4
|
-
# constructor wrapped self call (`Cons(head: f(h), tail: map(t, f))`) is rewritten
|
|
5
|
-
# to a loop (tail recursion modulo cons). All deep tests use 1,000,000 steps.
|
|
6
|
-
module tail_calls
|
|
7
|
-
import std/Int
|
|
8
|
-
import std/Float
|
|
9
|
-
import std/List
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
fun sumTo(n: Int, acc: Int) -> Int =
|
|
13
|
-
if n == 0 then acc else sumTo(n - 1, acc + n)
|
|
14
|
-
|
|
15
|
-
fun build(n: Int, acc: List[Int]) -> List[Int] =
|
|
16
|
-
if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
|
|
17
|
-
|
|
18
|
-
fun fromTo(a: Int, b: Int) -> List[Int] =
|
|
19
|
-
if a >= b then Nil else Cons(head: a, tail: fromTo(a + 1, b))
|
|
20
|
-
|
|
21
|
-
fun map(xs: List[A], f: fn(A) -> B) -> List[B] = match xs
|
|
22
|
-
Nil => Nil
|
|
23
|
-
Cons(h, t) => Cons(head: f(h), tail: map(t, f))
|
|
24
|
-
|
|
25
|
-
fun foldl(xs: List[T], acc: A, f: fn(A, T) -> A) -> A = match xs
|
|
26
|
-
Nil => acc
|
|
27
|
-
Cons(h, t) => foldl(t, f(acc, h), f)
|
|
28
|
-
|
|
29
|
-
fun length(xs: List[T], acc: Int) -> Int = match xs
|
|
30
|
-
Nil => acc
|
|
31
|
-
Cons(_, t) => length(t, acc + 1)
|
|
32
|
-
|
|
33
|
-
fun isEven(n: Int) -> Bool =
|
|
34
|
-
if n == 0 then true else isOdd(n - 1)
|
|
35
|
-
|
|
36
|
-
fun isOdd(n: Int) -> Bool =
|
|
37
|
-
if n == 0 then false else isEven(n - 1)
|
|
38
|
-
|
|
39
|
-
# `call` tail calls a closure value; `dec` tail calls `call`.
|
|
40
|
-
fun call(f: fn(Int) -> Int, n: Int) -> Int = f(n)
|
|
41
|
-
|
|
42
|
-
fun dec(n: Int) -> Int =
|
|
43
|
-
if n == 0 then 0 else call(dec, n - 1)
|
|
44
|
-
|
|
45
|
-
own record Counter(n: Int, total: Int)
|
|
46
|
-
|
|
47
|
-
fun spin(own c: Counter) -> Counter =
|
|
48
|
-
if c.n == 0 then c else spin(Counter(..c, n: c.n - 1, total: c.total + 2))
|
|
49
|
-
|
|
50
|
-
fun depth(n: Int) -> Int =
|
|
51
|
-
if n == 0 then 0 else 1 + depth(n - 1)
|
|
52
|
-
|
|
53
|
-
fun down(n: Int) -> error{Neg}!Int =
|
|
54
|
-
if n < 0
|
|
55
|
-
return error.Neg
|
|
56
|
-
if n == 0
|
|
57
|
-
return 5
|
|
58
|
-
down(n - 1)
|
|
59
|
-
|
|
60
|
-
test "tail_sum_million"
|
|
61
|
-
assert sumTo(1000000, 0) == 500000500000
|
|
62
|
-
|
|
63
|
-
test "tail_build_million"
|
|
64
|
-
assert length(build(1000000, Nil), 0) == 1000000
|
|
65
|
-
|
|
66
|
-
test "fold_million"
|
|
67
|
-
xs := fromTo(0, 1000000)
|
|
68
|
-
assert foldl(xs, 0, |a, x| a + x) == 499999500000
|
|
69
|
-
|
|
70
|
-
test "mutual_even_odd_million"
|
|
71
|
-
assert isEven(1000000)
|
|
72
|
-
assert !isOdd(1000000)
|
|
73
|
-
assert isOdd(999999)
|
|
74
|
-
|
|
75
|
-
test "trmc_map_million"
|
|
76
|
-
ys := map(fromTo(0, 1000000), |x| x + 1)
|
|
77
|
-
assert length(ys, 0) == 1000000
|
|
78
|
-
assert foldl(ys, 0, |a, x| a + x) == 500000500000
|
|
79
|
-
|
|
80
|
-
test "trmc_fromTo_million"
|
|
81
|
-
xs := fromTo(0, 1000000)
|
|
82
|
-
assert length(xs, 0) == 1000000
|
|
83
|
-
|
|
84
|
-
test "closure_tail_million"
|
|
85
|
-
assert dec(1000000) == 0
|
|
86
|
-
|
|
87
|
-
test "own_record_loop_100k"
|
|
88
|
-
c := spin(Counter(n: 100000, total: 0))
|
|
89
|
-
assert c.n == 0
|
|
90
|
-
assert c.total == 200000
|
|
91
|
-
|
|
92
|
-
test "recursive_closure_million"
|
|
93
|
-
go := |n, acc| if n == 0 then acc else go(n - 1, acc + n)
|
|
94
|
-
assert go(1000000, 0) == 500000500000
|
|
95
|
-
|
|
96
|
-
test "closure_body_tail_million"
|
|
97
|
-
fin := |n: Int| sumTo(n, 0)
|
|
98
|
-
assert fin(1000000) == 500000500000
|
|
99
|
-
|
|
100
|
-
test "error_union_loop_million"
|
|
101
|
-
assert { down(1000000) catch 99 } == 5
|
|
102
|
-
|
|
103
|
-
test "map_single_pass_million"
|
|
104
|
-
ys := map(fromTo(0, 1000000), |x| x * 2)
|
|
105
|
-
assert foldl(ys, 0, |a, x| a + x) == 999999000000
|
|
106
|
-
|
|
107
|
-
# Non-tail recursion still uses the wasm stack. Measured limit under the 4 MiB
|
|
108
|
-
# host stack: about 265,000 frames (260,000 passes, 270,000 traps) of `1 + depth(n - 1)`; tested well below it.
|
|
109
|
-
test "tree_recursion_depth_recorded"
|
|
110
|
-
assert depth(100000) == 100000
|
plum-examples/testing.plum
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Testing: native `test "name"` blocks and `assert` statements executed by
|
|
2
|
-
# the `plum test` runner.
|
|
3
|
-
import std/Str
|
|
4
|
-
import std/Int
|
|
5
|
-
import std/Float
|
|
6
|
-
|
|
7
|
-
fun add(a: Int, b: Int) -> Int =
|
|
8
|
-
a + b
|
|
9
|
-
|
|
10
|
-
fun isEven(n: Int) -> Bool =
|
|
11
|
-
n % 2 == 0
|
|
12
|
-
|
|
13
|
-
test "add works"
|
|
14
|
-
assert add(1, 2) == 3
|
|
15
|
-
assert add(2, 2) == 4
|
|
16
|
-
|
|
17
|
-
test "isEven works"
|
|
18
|
-
assert isEven(4)
|
|
19
|
-
assert !isEven(3)
|
|
20
|
-
assert isEven(2)
|
plum-examples/trait_scene.plum
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# A tiny pure scene: a `Scene` holds `Slice[Entity]`; the engine side knows only
|
|
2
|
-
# the trait, the records fill it in. No game host needed.
|
|
3
|
-
module trait_scene
|
|
4
|
-
import std/Str
|
|
5
|
-
import std/Int
|
|
6
|
-
import std/Slice
|
|
7
|
-
|
|
8
|
-
trait Entity(x: Int, y: Int) =
|
|
9
|
-
fun update(self, dt: Int)
|
|
10
|
-
fun draw(self) -> Int
|
|
11
|
-
fun hit(self, px: Int, py: Int) -> Bool = px == self.x && py == self.y
|
|
12
|
-
|
|
13
|
-
record House(Entity(x, y), floors: Int) =
|
|
14
|
-
override fun update(self, dt: Int) =
|
|
15
|
-
return
|
|
16
|
-
override fun draw(self) -> Int = self.floors * 10
|
|
17
|
-
|
|
18
|
-
record Walker(Entity(x, y), speed: Int) =
|
|
19
|
-
override fun update(self, dt: Int) =
|
|
20
|
-
self.x = self.x + self.speed * dt
|
|
21
|
-
override fun draw(self) -> Int = self.x
|
|
22
|
-
|
|
23
|
-
record Scene(entities: Slice[Entity]) =
|
|
24
|
-
fun update(self, dt: Int) =
|
|
25
|
-
for e := range self.entities
|
|
26
|
-
e.update(dt)
|
|
27
|
-
fun drawAll(self) -> Int =
|
|
28
|
-
total := 0
|
|
29
|
-
for e := range self.entities
|
|
30
|
-
total = total + e.draw()
|
|
31
|
-
total
|
|
32
|
-
|
|
33
|
-
fun makeScene() -> Scene =
|
|
34
|
-
es: Slice[Entity] := Slice[Entity]()
|
|
35
|
-
es.push(House(x: 0, y: 0, floors: 2))
|
|
36
|
-
es.push(Walker(x: 3, y: 1, speed: 2))
|
|
37
|
-
Scene(entities: es)
|
|
38
|
-
|
|
39
|
-
test "scene_updates_and_draws"
|
|
40
|
-
s := makeScene()
|
|
41
|
-
assert s.drawAll() == 23
|
|
42
|
-
s.update(5)
|
|
43
|
-
assert s.drawAll() == 33
|
|
44
|
-
|
|
45
|
-
test "scene_hit_default"
|
|
46
|
-
s := makeScene()
|
|
47
|
-
assert s.entities.at(1).hit(3, 1)
|
|
48
|
-
s.update(1)
|
|
49
|
-
assert s.entities.at(1).hit(5, 1)
|
|
50
|
-
assert !s.entities.at(0).hit(1, 1)
|
plum-examples/traits.plum
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
# Traits: fields, defaults, dispatch and a pure scene.
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Int
|
|
4
|
+
import std/Float
|
|
5
|
+
import std/Slice
|
|
6
|
+
|
|
7
|
+
# ==== traits ====
|
|
8
|
+
|
|
1
9
|
# Traits: `trait Entity(pos: Vec2, z: Int) = ...` bundles fields and methods. A
|
|
2
10
|
# `record` embeds traits by listing them first in its field list, gains their
|
|
3
11
|
# fields and default methods, and converts to each trait where one is expected.
|
|
4
12
|
# `Entity(pos, z)` exposes those trait fields as constructor parameters; a bare
|
|
5
13
|
# `Entity` exposes none, so the trait's or record's `fun init(self)` must set them.
|
|
6
14
|
# A method without a body is required; one with a body is a default.
|
|
7
|
-
module traits
|
|
8
|
-
import std/Str
|
|
9
|
-
import std/Int
|
|
10
|
-
import std/Float
|
|
11
|
-
import std/Slice
|
|
12
15
|
|
|
13
16
|
struct Vec2(x: Int, y: Int) =
|
|
14
17
|
fun add(self, o: Vec2) -> Vec2 = Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
15
18
|
|
|
16
|
-
trait Entity(pos: Vec2, z: Int) =
|
|
19
|
+
trait Entity(mut pos: Vec2, mut z: Int) =
|
|
17
20
|
fun update(self, dt: Int) -> Int
|
|
18
21
|
fun draw(self) -> Int
|
|
19
22
|
fun moveBy(self, d: Vec2) =
|
|
@@ -52,6 +55,44 @@ fun describe(e: Entity) -> Int =
|
|
|
52
55
|
House(_, _, _, kind) => kind
|
|
53
56
|
_ => -1
|
|
54
57
|
|
|
58
|
+
# ==== trait scene ====
|
|
59
|
+
|
|
60
|
+
# A tiny pure scene: a `Scene` holds `Slice[Actor]`; the engine side knows only
|
|
61
|
+
# the trait, the records fill it in. No game host needed.
|
|
62
|
+
|
|
63
|
+
trait Actor(mut x: Int, y: Int) =
|
|
64
|
+
fun update(self, dt: Int)
|
|
65
|
+
fun draw(self) -> Int
|
|
66
|
+
fun hit(self, px: Int, py: Int) -> Bool = px == self.x && py == self.y
|
|
67
|
+
|
|
68
|
+
record Hut(Actor(x, y), floors: Int) =
|
|
69
|
+
override fun update(self, dt: Int) =
|
|
70
|
+
return
|
|
71
|
+
override fun draw(self) -> Int = self.floors * 10
|
|
72
|
+
|
|
73
|
+
record Walker(Actor(x, y), speed: Int) =
|
|
74
|
+
override fun update(self, dt: Int) =
|
|
75
|
+
self.x = self.x + self.speed * dt
|
|
76
|
+
override fun draw(self) -> Int = self.x
|
|
77
|
+
|
|
78
|
+
record Scene(entities: Slice[Actor]) =
|
|
79
|
+
fun update(self, dt: Int) =
|
|
80
|
+
for e := range self.entities
|
|
81
|
+
e.update(dt)
|
|
82
|
+
fun drawAll(self) -> Int =
|
|
83
|
+
total := 0
|
|
84
|
+
for e := range self.entities
|
|
85
|
+
total = total + e.draw()
|
|
86
|
+
total
|
|
87
|
+
|
|
88
|
+
fun makeScene() -> Scene =
|
|
89
|
+
es: Slice[Actor] := Slice[Actor]()
|
|
90
|
+
es.push(Hut(x: 0, y: 0, floors: 2))
|
|
91
|
+
es.push(Walker(x: 3, y: 1, speed: 2))
|
|
92
|
+
Scene(entities: es)
|
|
93
|
+
|
|
94
|
+
# ==== traits tests ====
|
|
95
|
+
|
|
55
96
|
test "fields_through_trait"
|
|
56
97
|
h := house()
|
|
57
98
|
e: Entity := h
|
|
@@ -96,3 +137,18 @@ test "header_selection_init_and_internal_field"
|
|
|
96
137
|
assert c.update(1) == 9
|
|
97
138
|
e: Entity := c
|
|
98
139
|
assert e.draw() == 9
|
|
140
|
+
|
|
141
|
+
# ==== trait scene tests ====
|
|
142
|
+
|
|
143
|
+
test "scene_updates_and_draws"
|
|
144
|
+
s := makeScene()
|
|
145
|
+
assert s.drawAll() == 23
|
|
146
|
+
s.update(5)
|
|
147
|
+
assert s.drawAll() == 33
|
|
148
|
+
|
|
149
|
+
test "scene_hit_default"
|
|
150
|
+
s := makeScene()
|
|
151
|
+
assert s.entities.at(1).hit(3, 1)
|
|
152
|
+
s.update(1)
|
|
153
|
+
assert s.entities.at(1).hit(5, 1)
|
|
154
|
+
assert !s.entities.at(0).hit(1, 1)
|
plum-examples/typed_arrays.plum
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Typed arrays: unboxed native `Array[Int]` and `Array[Float]` lowering to
|
|
2
|
-
# WebAssembly primitive arrays, along with flat 2D `Grid[T]`.
|
|
3
|
-
module typed_arrays
|
|
4
|
-
import std/Int
|
|
5
|
-
import std/Float
|
|
6
|
-
import std/Array
|
|
7
|
-
import std/Grid
|
|
8
|
-
import std/Seq
|
|
9
|
-
|
|
10
|
-
# Tile-grid style data: Array[Int] / Array[Float] / Array[Bool] are native
|
|
11
|
-
# unboxed wasm arrays (i64 / f64 / i32 elements), not boxed anyref arrays.
|
|
12
|
-
fun sumInts(n: Int) -> Int =
|
|
13
|
-
a := Array.filled(n, 3)
|
|
14
|
-
total := 0
|
|
15
|
-
for i := range n
|
|
16
|
-
total = total + a.get(i)
|
|
17
|
-
return total
|
|
18
|
-
|
|
19
|
-
fun sumFloats(n: Int) -> Float =
|
|
20
|
-
a := Array.filled(n, 0.5)
|
|
21
|
-
total := 0.0
|
|
22
|
-
for i := range n
|
|
23
|
-
total = total + a.get(i)
|
|
24
|
-
return total
|
|
25
|
-
|
|
26
|
-
fun countTrue(n: Int) -> Int =
|
|
27
|
-
a := Array.filled(n, false)
|
|
28
|
-
a.set(1, true)
|
|
29
|
-
a.set(3, true)
|
|
30
|
-
c := 0
|
|
31
|
-
for i := range n
|
|
32
|
-
if a.get(i)
|
|
33
|
-
c = c + 1
|
|
34
|
-
return c
|
|
35
|
-
|
|
36
|
-
test "array_int_native: filled arrays hold their values"
|
|
37
|
-
assert sumInts(10) == 30
|
|
38
|
-
assert sumFloats(4) == 2.0
|
|
39
|
-
assert countTrue(5) == 2
|
plum-examples/unboxed_variants.plum
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Probes for unboxed variants (union-enum members and inline variants are
|
|
2
2
|
# subtypes of the enum): deep nesting, Json round trips, toStr, a million-long
|
|
3
|
-
# List of unions, member mutation, reassignment across members,
|
|
3
|
+
# List of unions, member mutation, reassignment across members, record
|
|
4
4
|
# members, and enum `init` versus variant-name resolution.
|
|
5
5
|
module unboxed_variants
|
|
6
6
|
import std/Str
|
|
@@ -175,9 +175,9 @@ fun leaves(n: Int) -> List[Int] =
|
|
|
175
175
|
if n == 0 then Nil else Cons(head: n, tail: leaves(n - 1))
|
|
176
176
|
|
|
177
177
|
# State.
|
|
178
|
-
record Circle(r: Float)
|
|
178
|
+
record Circle(mut r: Float)
|
|
179
179
|
|
|
180
|
-
record Rect(w: Float, h: Float)
|
|
180
|
+
record Rect(mut w: Float, h: Float)
|
|
181
181
|
|
|
182
182
|
enum Shape = Circle | Rect
|
|
183
183
|
|
|
@@ -200,13 +200,13 @@ fun kind(s: Shape) -> Int =
|
|
|
200
200
|
Circle(_) => 1
|
|
201
201
|
Rect(_, _) => 10
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
record Token(id: Int)
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
record Stamp(id: Int, note: Str)
|
|
206
206
|
|
|
207
207
|
enum Mark = Token | Stamp
|
|
208
208
|
|
|
209
|
-
fun takeMark(
|
|
209
|
+
fun takeMark(m: Mark) -> Mark = m
|
|
210
210
|
|
|
211
211
|
fun markId(m: Mark) -> Int =
|
|
212
212
|
match m
|