Skip to content

Commit

Permalink
Add REPL (scripting) to devtools
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
elgopher committed Aug 12, 2023
1 parent ba02f4f commit 376b235
Show file tree
Hide file tree
Showing 47 changed files with 2,692 additions and 11 deletions.
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"console": "integratedTerminal"
}
]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Pi is under development. Only limited functionality is provided. API is not stab
## How to get started?

1. Install dependencies
* [Go 1.18+](https://go.dev/dl/)
* [Go 1.20+](https://go.dev/dl/)
* If not on Windows, please install additional dependencies for [Linux](docs/install-linux.md) or [macOS](docs/install-macos.md).
2. Try examples from [examples](examples) directory.
3. Create a new game using provided [Github template](https://github.com/elgopher/pi-template).
Expand Down
11 changes: 11 additions & 0 deletions devtools/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package devtools

import (
"fmt"

"github.com/elgopher/pi"
"github.com/elgopher/pi/devtools/internal/snapshot"
)
Expand All @@ -13,7 +15,15 @@ var (
timeWhenPaused float64
)

var helpShown bool

func pauseGame() {
fmt.Println("Game paused")
if !helpShown {
helpShown = true
fmt.Println("\nPress right mouse button in the game window to show the toolbar.")
fmt.Println("Press P in the game window to take screenshot.")
}
gamePaused = true
timeWhenPaused = pi.TimeSeconds
snapshot.Take()
Expand All @@ -23,4 +33,5 @@ func resumeGame() {
gamePaused = false
pi.TimeSeconds = timeWhenPaused
snapshot.Draw()
fmt.Println("Game resumed")
}
15 changes: 14 additions & 1 deletion devtools/devtools.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/elgopher/pi"
"github.com/elgopher/pi/devtools/internal/inspector"
"github.com/elgopher/pi/devtools/internal/terminal"
)

var (
Expand All @@ -33,7 +34,8 @@ func MustRun(runBackend func() error) {
}

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

pi.Update = func() {
updateDevTools()
Expand All @@ -53,6 +55,17 @@ func MustRun(runBackend func() error) {
}
}

if err := interpreterInstance.SetUpdate(&update); err != nil {
panic(fmt.Sprintf("problem exporting Update function: %s", err))
}

if err := interpreterInstance.SetDraw(&draw); err != nil {
panic(fmt.Sprintf("problem exporting Draw function: %s", err))
}

terminal.StartReadingCommands()
defer terminal.StopReadingCommandsFromStdin()

if err := runBackend(); err != nil {
panic(fmt.Sprintf("Something terrible happened! Pi cannot be run: %v\n", err))
}
Expand Down
124 changes: 124 additions & 0 deletions devtools/internal/help/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// (c) 2023 Jacek Olszak
// This code is licensed under MIT license (see LICENSE for details)

package help

import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"sort"
"strings"

"github.com/elgopher/pi/devtools/internal/lib"
)

var NotFound = fmt.Errorf("no help found")

func PrintHelp(topic string) error {
switch topic {
case "":
fmt.Println("This is interactive terminal. " +
"You can write Go code here, which will run immediately. " +
"You can use all Pi packages: pi, key, state, snap, font, image and " +
"selection of standard packages: " + strings.Join(stdPackages(), ", ") + ". " +
"\n\n" +
"Type help topic for more information. For example: help pi or help pi.Spr" +
"\n\n" +
"Available commands: help [h], pause [p], resume [r], undo [u]",
)
return nil
default:
return goDoc(topic)
}
}

func stdPackages() []string {
var packages []string
for _, p := range lib.AllPackages() {
if p.IsStdPackage() {
packages = append(packages, p.Alias)
}
}
sort.Strings(packages)
return packages
}

func goDoc(symbol string) error {
symbol = completeSymbol(symbol)
if symbolNotSupported(symbol) {
return NotFound
}

fmt.Println("###############################################################################")

var args []string
args = append(args, "doc")
if shouldShowDetailedDescriptionForSymbol(symbol) {
args = append(args, "-all")
}
args = append(args, symbol)
command := exec.Command("go", args...)
command.Stdout = bufio.NewWriter(os.Stdout)

if err := command.Run(); err != nil {
var exitErr *exec.ExitError
if isExitErr := errors.As(err, &exitErr); isExitErr && exitErr.ExitCode() == 1 {
return NotFound
}

return fmt.Errorf("problem getting help: %w", err)
}

return nil
}

func completeSymbol(symbol string) string {
packages := lib.AllPackages()

for _, p := range packages {
if p.Alias == symbol {
return p.Path
}
}

for _, p := range packages {
prefix := p.Alias + "."
if strings.HasPrefix(symbol, prefix) {
return p.Path + "." + symbol[len(prefix):]
}
}

return symbol
}

func symbolNotSupported(symbol string) bool {
packages := lib.AllPackages()

for _, p := range packages {
prefix := p.Path + "."
if strings.HasPrefix(symbol, prefix) || symbol == p.Path {
return false
}
}

return true
}

var symbolsWithDetailedDescription = []string{
"github.com/elgopher/pi.Button",
"github.com/elgopher/pi.MouseButton",
"github.com/elgopher/pi/key.Button",
}

func shouldShowDetailedDescriptionForSymbol(symbol string) bool {
for _, s := range symbolsWithDetailedDescription {
if symbol == s {
return true
}
}

return false
}
106 changes: 106 additions & 0 deletions devtools/internal/help/help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// (c) 2023 Jacek Olszak
// This code is licensed under MIT license (see LICENSE for details)

//go:build !js

package help_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/elgopher/pi/devtools/internal/help"
"github.com/elgopher/pi/devtools/internal/test"
)

func TestPrintHelp(t *testing.T) {
t.Run("should return error when trying to print help for not imported packages", func(t *testing.T) {
topics := []string{
"io", "io.Writer",
}
for _, topic := range topics {
t.Run(topic, func(t *testing.T) {
// when
err := help.PrintHelp(topic)
// then
assert.ErrorIs(t, err, help.NotFound)
})
}
})

t.Run("should return error when trying to print help for non-existent symbol", func(t *testing.T) {
err := help.PrintHelp("pi.NonExistent")
assert.ErrorIs(t, err, help.NotFound)
})

t.Run("should print help for", func(t *testing.T) {
tests := map[string]struct {
topic string
expected string
}{
"package": {
topic: "pi",
expected: `Package pi`,
},
"function": {
topic: "pi.Spr",
expected: `func Spr(n, x, y int)`,
},
"struct": {
topic: "pi.PixMap",
expected: `type PixMap struct {`,
},
}
for testName, testCase := range tests {
t.Run(testName, func(t *testing.T) {
swapper := test.SwapStdout(t)
// when
err := help.PrintHelp(testCase.topic)
// then
swapper.BringStdoutBack()
assert.NoError(t, err)
output := swapper.ReadOutput(t)
assert.Contains(t, output, testCase.expected)
})
}
})

t.Run("should show help for image.Image from github.com/elgopher/pi package, not from stdlib", func(t *testing.T) {
topics := []string{
"image", "image.Image",
}
for _, topic := range topics {
t.Run(topic, func(t *testing.T) {
swapper := test.SwapStdout(t)
// when
err := help.PrintHelp("image.Image")
// then
swapper.BringStdoutBack()
assert.NoError(t, err)
output := swapper.ReadOutput(t)
assert.Contains(t, output, `// import "github.com/elgopher/pi/image"`)
})
}
})

t.Run("should show detailed help for pi.Button", func(t *testing.T) {
tests := map[string]string{
"pi.Button": "Keyboard mappings",
"pi.MouseButton": "MouseRight MouseButton = 2",
"key.Button": "func (b Button) String() string",
}
for topic, expected := range tests {
t.Run(topic, func(t *testing.T) {
swapper := test.SwapStdout(t)
// when
err := help.PrintHelp(topic)
// then
swapper.BringStdoutBack()
assert.NoError(t, err)
output := swapper.ReadOutput(t)
assert.Contains(t, output, expected)
})
}
})
}
2 changes: 1 addition & 1 deletion devtools/internal/inspector/measure.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (m *Measure) Update() {
case pi.MouseBtnp(pi.MouseLeft) && !distance.measuring:
distance.measuring = true
distance.startX, distance.startY = x, y
fmt.Printf("Measuring started at (%d, %d)\n", x, y)
fmt.Printf("\nMeasuring started at (%d, %d)\n", x, y)
case !pi.MouseBtn(pi.MouseLeft) && distance.measuring:
distance.measuring = false
dist, width, height := calcDistance()
Expand Down
8 changes: 0 additions & 8 deletions devtools/internal/inspector/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package inspector

import (
"fmt"
"math"

"github.com/elgopher/pi"
Expand Down Expand Up @@ -33,14 +32,7 @@ func calcDistance() (dist float64, width, height int) {
return
}

var helpShown bool

func Update() {
if !helpShown {
helpShown = true
fmt.Println("Press right mouse button to show toolbar.")
fmt.Println("Press P to take screenshot.")
}

if !toolbar.visible {
tool.Update()
Expand Down
12 changes: 12 additions & 0 deletions devtools/internal/interpreter/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// (c) 2023 Jacek Olszak
// This code is licensed under MIT license (see LICENSE for details)

package interpreter

type ErrInvalidIdentifier struct {
message string
}

func (e ErrInvalidIdentifier) Error() string {
return e.message
}
Loading

0 comments on commit 376b235

Please sign in to comment.