Skip to content

Commit 376b235

Browse files
committed
Add REPL (scripting) to devtools
This commit adds the ability to run Go code when the game is running. No need to compile, and restart the game. Just write the code in terminal, hit enter, and it will be immediately executed. Go code is interpreted by traefik/yaegi interpreter. It is a Go interpreter compatible with standard Go compiler. Lines are read from terminal by peterh/liner package. For now I'm using my fork of github.com/peterh/liner, because the original does not work well in Goland/VSCode on Win11 (see peterh/liner#163). This is temporary, until peterh/liner is fixed properly. Limitations: * MidInt, MaxInt, MinInt - those functions can be executed in terminal but only accepts int's (not int64, byte etc.) * pi.Int cannot be used * The size of compiled game with devtools is increased by 3.7MB (+35% increase), the compilation takes 21% more time. But it is worth it. I plan to add a possibility to disable scripting in the future though.
1 parent ba02f4f commit 376b235

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2692
-11
lines changed

.vscode/launch.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Launch Package",
9+
"type": "go",
10+
"request": "launch",
11+
"mode": "auto",
12+
"program": "${fileDirname}",
13+
"console": "integratedTerminal"
14+
}
15+
]
16+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Pi is under development. Only limited functionality is provided. API is not stab
3939
## How to get started?
4040

4141
1. Install dependencies
42-
* [Go 1.18+](https://go.dev/dl/)
42+
* [Go 1.20+](https://go.dev/dl/)
4343
* If not on Windows, please install additional dependencies for [Linux](docs/install-linux.md) or [macOS](docs/install-macos.md).
4444
2. Try examples from [examples](examples) directory.
4545
3. Create a new game using provided [Github template](https://github.com/elgopher/pi-template).

devtools/control.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package devtools
55

66
import (
7+
"fmt"
8+
79
"github.com/elgopher/pi"
810
"github.com/elgopher/pi/devtools/internal/snapshot"
911
)
@@ -13,7 +15,15 @@ var (
1315
timeWhenPaused float64
1416
)
1517

18+
var helpShown bool
19+
1620
func pauseGame() {
21+
fmt.Println("Game paused")
22+
if !helpShown {
23+
helpShown = true
24+
fmt.Println("\nPress right mouse button in the game window to show the toolbar.")
25+
fmt.Println("Press P in the game window to take screenshot.")
26+
}
1727
gamePaused = true
1828
timeWhenPaused = pi.TimeSeconds
1929
snapshot.Take()
@@ -23,4 +33,5 @@ func resumeGame() {
2333
gamePaused = false
2434
pi.TimeSeconds = timeWhenPaused
2535
snapshot.Draw()
36+
fmt.Println("Game resumed")
2637
}

devtools/devtools.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/elgopher/pi"
1010
"github.com/elgopher/pi/devtools/internal/inspector"
11+
"github.com/elgopher/pi/devtools/internal/terminal"
1112
)
1213

1314
var (
@@ -33,7 +34,8 @@ func MustRun(runBackend func() error) {
3334
}
3435

3536
inspector.BgColor, inspector.FgColor = BgColor, FgColor
36-
fmt.Println("Press F12 to pause the game and show devtools.")
37+
fmt.Println("Press F12 in the game window to pause the game and activate devtools inspector.")
38+
fmt.Println("Terminal activated. Type help for help.")
3739

3840
pi.Update = func() {
3941
updateDevTools()
@@ -53,6 +55,17 @@ func MustRun(runBackend func() error) {
5355
}
5456
}
5557

58+
if err := interpreterInstance.SetUpdate(&update); err != nil {
59+
panic(fmt.Sprintf("problem exporting Update function: %s", err))
60+
}
61+
62+
if err := interpreterInstance.SetDraw(&draw); err != nil {
63+
panic(fmt.Sprintf("problem exporting Draw function: %s", err))
64+
}
65+
66+
terminal.StartReadingCommands()
67+
defer terminal.StopReadingCommandsFromStdin()
68+
5669
if err := runBackend(); err != nil {
5770
panic(fmt.Sprintf("Something terrible happened! Pi cannot be run: %v\n", err))
5871
}

devtools/internal/help/help.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// (c) 2023 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package help
5+
6+
import (
7+
"bufio"
8+
"errors"
9+
"fmt"
10+
"os"
11+
"os/exec"
12+
"sort"
13+
"strings"
14+
15+
"github.com/elgopher/pi/devtools/internal/lib"
16+
)
17+
18+
var NotFound = fmt.Errorf("no help found")
19+
20+
func PrintHelp(topic string) error {
21+
switch topic {
22+
case "":
23+
fmt.Println("This is interactive terminal. " +
24+
"You can write Go code here, which will run immediately. " +
25+
"You can use all Pi packages: pi, key, state, snap, font, image and " +
26+
"selection of standard packages: " + strings.Join(stdPackages(), ", ") + ". " +
27+
"\n\n" +
28+
"Type help topic for more information. For example: help pi or help pi.Spr" +
29+
"\n\n" +
30+
"Available commands: help [h], pause [p], resume [r], undo [u]",
31+
)
32+
return nil
33+
default:
34+
return goDoc(topic)
35+
}
36+
}
37+
38+
func stdPackages() []string {
39+
var packages []string
40+
for _, p := range lib.AllPackages() {
41+
if p.IsStdPackage() {
42+
packages = append(packages, p.Alias)
43+
}
44+
}
45+
sort.Strings(packages)
46+
return packages
47+
}
48+
49+
func goDoc(symbol string) error {
50+
symbol = completeSymbol(symbol)
51+
if symbolNotSupported(symbol) {
52+
return NotFound
53+
}
54+
55+
fmt.Println("###############################################################################")
56+
57+
var args []string
58+
args = append(args, "doc")
59+
if shouldShowDetailedDescriptionForSymbol(symbol) {
60+
args = append(args, "-all")
61+
}
62+
args = append(args, symbol)
63+
command := exec.Command("go", args...)
64+
command.Stdout = bufio.NewWriter(os.Stdout)
65+
66+
if err := command.Run(); err != nil {
67+
var exitErr *exec.ExitError
68+
if isExitErr := errors.As(err, &exitErr); isExitErr && exitErr.ExitCode() == 1 {
69+
return NotFound
70+
}
71+
72+
return fmt.Errorf("problem getting help: %w", err)
73+
}
74+
75+
return nil
76+
}
77+
78+
func completeSymbol(symbol string) string {
79+
packages := lib.AllPackages()
80+
81+
for _, p := range packages {
82+
if p.Alias == symbol {
83+
return p.Path
84+
}
85+
}
86+
87+
for _, p := range packages {
88+
prefix := p.Alias + "."
89+
if strings.HasPrefix(symbol, prefix) {
90+
return p.Path + "." + symbol[len(prefix):]
91+
}
92+
}
93+
94+
return symbol
95+
}
96+
97+
func symbolNotSupported(symbol string) bool {
98+
packages := lib.AllPackages()
99+
100+
for _, p := range packages {
101+
prefix := p.Path + "."
102+
if strings.HasPrefix(symbol, prefix) || symbol == p.Path {
103+
return false
104+
}
105+
}
106+
107+
return true
108+
}
109+
110+
var symbolsWithDetailedDescription = []string{
111+
"github.com/elgopher/pi.Button",
112+
"github.com/elgopher/pi.MouseButton",
113+
"github.com/elgopher/pi/key.Button",
114+
}
115+
116+
func shouldShowDetailedDescriptionForSymbol(symbol string) bool {
117+
for _, s := range symbolsWithDetailedDescription {
118+
if symbol == s {
119+
return true
120+
}
121+
}
122+
123+
return false
124+
}

devtools/internal/help/help_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// (c) 2023 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
//go:build !js
5+
6+
package help_test
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
13+
"github.com/elgopher/pi/devtools/internal/help"
14+
"github.com/elgopher/pi/devtools/internal/test"
15+
)
16+
17+
func TestPrintHelp(t *testing.T) {
18+
t.Run("should return error when trying to print help for not imported packages", func(t *testing.T) {
19+
topics := []string{
20+
"io", "io.Writer",
21+
}
22+
for _, topic := range topics {
23+
t.Run(topic, func(t *testing.T) {
24+
// when
25+
err := help.PrintHelp(topic)
26+
// then
27+
assert.ErrorIs(t, err, help.NotFound)
28+
})
29+
}
30+
})
31+
32+
t.Run("should return error when trying to print help for non-existent symbol", func(t *testing.T) {
33+
err := help.PrintHelp("pi.NonExistent")
34+
assert.ErrorIs(t, err, help.NotFound)
35+
})
36+
37+
t.Run("should print help for", func(t *testing.T) {
38+
tests := map[string]struct {
39+
topic string
40+
expected string
41+
}{
42+
"package": {
43+
topic: "pi",
44+
expected: `Package pi`,
45+
},
46+
"function": {
47+
topic: "pi.Spr",
48+
expected: `func Spr(n, x, y int)`,
49+
},
50+
"struct": {
51+
topic: "pi.PixMap",
52+
expected: `type PixMap struct {`,
53+
},
54+
}
55+
for testName, testCase := range tests {
56+
t.Run(testName, func(t *testing.T) {
57+
swapper := test.SwapStdout(t)
58+
// when
59+
err := help.PrintHelp(testCase.topic)
60+
// then
61+
swapper.BringStdoutBack()
62+
assert.NoError(t, err)
63+
output := swapper.ReadOutput(t)
64+
assert.Contains(t, output, testCase.expected)
65+
})
66+
}
67+
})
68+
69+
t.Run("should show help for image.Image from github.com/elgopher/pi package, not from stdlib", func(t *testing.T) {
70+
topics := []string{
71+
"image", "image.Image",
72+
}
73+
for _, topic := range topics {
74+
t.Run(topic, func(t *testing.T) {
75+
swapper := test.SwapStdout(t)
76+
// when
77+
err := help.PrintHelp("image.Image")
78+
// then
79+
swapper.BringStdoutBack()
80+
assert.NoError(t, err)
81+
output := swapper.ReadOutput(t)
82+
assert.Contains(t, output, `// import "github.com/elgopher/pi/image"`)
83+
})
84+
}
85+
})
86+
87+
t.Run("should show detailed help for pi.Button", func(t *testing.T) {
88+
tests := map[string]string{
89+
"pi.Button": "Keyboard mappings",
90+
"pi.MouseButton": "MouseRight MouseButton = 2",
91+
"key.Button": "func (b Button) String() string",
92+
}
93+
for topic, expected := range tests {
94+
t.Run(topic, func(t *testing.T) {
95+
swapper := test.SwapStdout(t)
96+
// when
97+
err := help.PrintHelp(topic)
98+
// then
99+
swapper.BringStdoutBack()
100+
assert.NoError(t, err)
101+
output := swapper.ReadOutput(t)
102+
assert.Contains(t, output, expected)
103+
})
104+
}
105+
})
106+
}

devtools/internal/inspector/measure.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (m *Measure) Update() {
9494
case pi.MouseBtnp(pi.MouseLeft) && !distance.measuring:
9595
distance.measuring = true
9696
distance.startX, distance.startY = x, y
97-
fmt.Printf("Measuring started at (%d, %d)\n", x, y)
97+
fmt.Printf("\nMeasuring started at (%d, %d)\n", x, y)
9898
case !pi.MouseBtn(pi.MouseLeft) && distance.measuring:
9999
distance.measuring = false
100100
dist, width, height := calcDistance()

devtools/internal/inspector/update.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package inspector
55

66
import (
7-
"fmt"
87
"math"
98

109
"github.com/elgopher/pi"
@@ -33,14 +32,7 @@ func calcDistance() (dist float64, width, height int) {
3332
return
3433
}
3534

36-
var helpShown bool
37-
3835
func Update() {
39-
if !helpShown {
40-
helpShown = true
41-
fmt.Println("Press right mouse button to show toolbar.")
42-
fmt.Println("Press P to take screenshot.")
43-
}
4436

4537
if !toolbar.visible {
4638
tool.Update()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// (c) 2023 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package interpreter
5+
6+
type ErrInvalidIdentifier struct {
7+
message string
8+
}
9+
10+
func (e ErrInvalidIdentifier) Error() string {
11+
return e.message
12+
}

0 commit comments

Comments
 (0)