Pulpscript How-To: Creating and Editing Code in Bulk

There was some interest in this topic on the Pulp discord channel, so I'm posting my tips and tricks here. Not saying it's the best way, just sharing what I've arrived at. All key commands refer to Mac keyboard layout.

What's this all about?

I'm making a tile-based puzzle game [EDIT: now released!], and I wanted to implement an undo feature so that the player can revert a move and go back one "step" in time. You can see a gif of this in action here .

Pulpscript is very simple, so for more complex or "dynamic" behavior like this, you have to write it out yourself. Explicitly defining every variable and event handler that could be used is tedious to do by hand.

For the undo feature, we need to keep track of the game's state at each step in time, which means we need to save many tile states at each step in time, which means we need lots and lots of variables. How many? For full room states, it's roughly 25 (tiles wide) * 15 (tiles high) * the number of states to save (the max number of times the player can undo), plus a bunch of special handling code for the player!

Even for a smaller example like a 3x3 Tic-tac-toe board, that's a lot of variables to manipulate. With just a few undos, some of the needed Pulpscript might look something like this:

Big chunk of Pulpscript
// ...

on setStateT0ToCurrent do
	// log "@setStateT0ToCurrent"
	stateT0_player_x = playerStateLatest_x
	stateT0_player_y = playerStateLatest_y
	stateT0_x_0_y_0 = name 0,0
	stateT0_x_0_y_1 = name 0,1
	stateT0_x_0_y_2 = name 0,2
	stateT0_x_1_y_0 = name 1,0
	stateT0_x_1_y_1 = name 1,1
	stateT0_x_1_y_2 = name 1,2
	stateT0_x_2_y_0 = name 2,0
	stateT0_x_2_y_1 = name 2,1
	stateT0_x_2_y_2 = name 2,2
end

on setStateT1ToCurrent do
	// log "@setStateT1ToCurrent"
	stateT1_player_x = playerStateLatest_x
	stateT1_player_y = playerStateLatest_y
	stateT1_x_0_y_0 = name 0,0
	stateT1_x_0_y_1 = name 0,1
	stateT1_x_0_y_2 = name 0,2
	stateT1_x_1_y_0 = name 1,0
	stateT1_x_1_y_1 = name 1,1
	stateT1_x_1_y_2 = name 1,2
	stateT1_x_2_y_0 = name 2,0
	stateT1_x_2_y_1 = name 2,1
	stateT1_x_2_y_2 = name 2,2
end

on setStateT2ToCurrent do
	// log "@setStateT2ToCurrent"
	stateT2_player_x = playerStateLatest_x
	stateT2_player_y = playerStateLatest_y
	stateT2_x_0_y_0 = name 0,0
	stateT2_x_0_y_1 = name 0,1
	stateT2_x_0_y_2 = name 0,2
	stateT2_x_1_y_0 = name 1,0
	stateT2_x_1_y_1 = name 1,1
	stateT2_x_1_y_2 = name 1,2
	stateT2_x_2_y_0 = name 2,0
	stateT2_x_2_y_1 = name 2,1
	stateT2_x_2_y_2 = name 2,2
end

on setStateT3ToCurrent do
	// log "@setStateT3ToCurrent"
	stateT3_player_x = playerStateLatest_x
	stateT3_player_y = playerStateLatest_y
	stateT3_x_0_y_0 = name 0,0
	stateT3_x_0_y_1 = name 0,1
	stateT3_x_0_y_2 = name 0,2
	stateT3_x_1_y_0 = name 1,0
	stateT3_x_1_y_1 = name 1,1
	stateT3_x_1_y_2 = name 1,2
	stateT3_x_2_y_0 = name 2,0
	stateT3_x_2_y_1 = name 2,1
	stateT3_x_2_y_2 = name 2,2
end

on setCurrentToT0 do
	// log "@setCurrentToT0"
	goto stateT0_player_x,stateT0_player_y
	playerStateLatest_x = stateT0_player_x
	playerStateLatest_y = stateT0_player_y
    tell 0,0 to
        swap stateT0_x_0_y_0
    end
    tell 0,1 to
        swap stateT0_x_0_y_1
    end
    tell 0,2 to
        swap stateT0_x_0_y_2
    end
    tell 1,0 to
        swap stateT0_x_1_y_0
    end
    tell 1,1 to
        swap stateT0_x_1_y_1
    end
    tell 1,2 to
        swap stateT0_x_1_y_2
    end
    tell 2,0 to
        swap stateT0_x_2_y_0
    end
    tell 2,1 to
        swap stateT0_x_2_y_1
    end
    tell 2,2 to
        swap stateT0_x_2_y_2
    end
end

on setCurrentToT1 do
	// log "@setCurrentToT1"
	goto stateT1_player_x,stateT1_player_y
	playerStateLatest_x = stateT1_player_x
	playerStateLatest_y = stateT1_player_y
    tell 0,0 to
        swap stateT1_x_0_y_0
    end
    tell 0,1 to
        swap stateT1_x_0_y_1
    end
    tell 0,2 to
        swap stateT1_x_0_y_2
    end
    tell 1,0 to
        swap stateT1_x_1_y_0
    end
    tell 1,1 to
        swap stateT1_x_1_y_1
    end
    tell 1,2 to
        swap stateT1_x_1_y_2
    end
    tell 2,0 to
        swap stateT1_x_2_y_0
    end
    tell 2,1 to
        swap stateT1_x_2_y_1
    end
    tell 2,2 to
        swap stateT1_x_2_y_2
    end
end

on setCurrentToT2 do
	// log "@setCurrentToT2"
	goto stateT2_player_x,stateT2_player_y
	playerStateLatest_x = stateT2_player_x
	playerStateLatest_y = stateT2_player_y
    tell 0,0 to
        swap stateT2_x_0_y_0
    end
    tell 0,1 to
        swap stateT2_x_0_y_1
    end
    tell 0,2 to
        swap stateT2_x_0_y_2
    end
    tell 1,0 to
        swap stateT2_x_1_y_0
    end
    tell 1,1 to
        swap stateT2_x_1_y_1
    end
    tell 1,2 to
        swap stateT2_x_1_y_2
    end
    tell 2,0 to
        swap stateT2_x_2_y_0
    end
    tell 2,1 to
        swap stateT2_x_2_y_1
    end
    tell 2,2 to
        swap stateT2_x_2_y_2
    end
end

on setCurrentToT3 do
	// log "@setCurrentToT3"
	goto stateT3_player_x,stateT3_player_y
	playerStateLatest_x = stateT3_player_x
	playerStateLatest_y = stateT3_player_y
    tell 0,0 to
        swap stateT3_x_0_y_0
    end
    tell 0,1 to
        swap stateT3_x_0_y_1
    end
    tell 0,2 to
        swap stateT3_x_0_y_2
    end
    tell 1,0 to
        swap stateT3_x_1_y_0
    end
    tell 1,1 to
        swap stateT3_x_1_y_1
    end
    tell 1,2 to
        swap stateT3_x_1_y_2
    end
    tell 2,0 to
        swap stateT3_x_2_y_0
    end
    tell 2,1 to
        swap stateT3_x_2_y_1
    end
    tell 2,2 to
        swap stateT3_x_2_y_2
    end
end


Here are a couple ways to make that scripting less tedious.

Tip 1: Multi-cursor text manipulations

This one is generally applicable to text editing, not just Pulpscript. Many text editors let you place multiple cursors down at once, so you can edit multiple lines at once.

You can try this out in the Pulpscript editor by holding down the Command key (โŒ˜) and clicking on multiple places to place cursors. You can also hold the Option key and press the Up and Down arrows to add cursors above and below your current cursor. The Pulpscript editor uses Ace which lists shortcuts here. That link is currently broken, but you can find other references online (like here and in the official source).

You can also copy and paste your code into your editor of choice. Just to give a few more examples:

  1. Here's what multi-cursor editing looks like in Panic's Nova editor: link
  2. In Sublime Text, pressing Command+D (โŒ˜D) on a word will highlight that word, and subsequent presses will additionally select the next occurrences of that word, giving you cursors at all of them.
  3. In VS Code if your cursor is on a word, you can press Command+Shift+L (โ‡งโŒ˜L) and it will select all occurrences of that word in the file, giving you a cursor at each location instantly.

Learn the keyboard shortcuts for these operations, since you'll use them often once you get the hang of this.

Once you have multiple cursors placed, you can move them around as normal. This is an easy way to duplicate some big chunk of code, then make some slight change to the multiple copied lines.

Smooth moves

You'll soon find that sometimes you need to navigate around "words" (groups of certain characters next to each other) of different lengths.

// we have:
hoge-1 = ab
//...
hoge-21 = cdefg
// we want:
// hoge-1-x = ab
//...
// hoge-21-x = cdefg

Imagine we want to add "-x" after the numbers in two variable names "hoge-1" and "hoge-21" we have on two separate lines, so that they become "hoge-1-x" and "hoge-21-x", and we have two cursors, both directly to the left of the number in each variable name. In this case, hold Option before pressing right and typing the "-x". This moves each cursor by word, instead of by character, so that we don't end up changing these names to "hoge-1-x" and "hoge-2-x1" by mistake. This generally works for any text input in any program, though the characters used for word-separating can differ (like hyphens, underscores, other special characters). Also, holding Command before pressing left or right will move all cursors to the beginning or end of their lines.

You can also paste multiple different things into the places your cursors are highlighting, as long as the number of things to paste matches the number of cursors. This might also depend on the editor used.

In the below example, selecting the 3 numbers with 3 cursors, cutting or copying, then selecting the 3 'X's with 3 cursors and pasting should result in the numbers getting cleanly inserted into the corresponding "slots".

// before

2
4
6

hoge-X
hoge-X
hoge-X

// after

hoge-2
hoge-4
hoge-6

A word of caution

Bulk copy-pasting between sources does introduce the risk of accidentally overwriting your latest changes in one window with old changes from another window, so be careful! Also, I noticed that as a result of this copy-paste workflow, I've personally tended towards a smaller number of larger Pulpscript files with lots of logic inside, instead of spreading the code out over scripts for multiple rooms and items (which would require more copying and pasting).

Tip 2: Generating Pulpscript via other scripting

Less generally applicable than the first tip, but this helps when you know exactly what code you want to write, and it's just a matter of churning it out.

If you're familiar with another programming language, consider using it to print out large blocks of Pulpscript, and paste that into the Pulp editor. Most languages have string formatting functions, and you could maybe even do this in Pulpscript itself, using log statements and copying the output from the console!

I used Python below since it's simple and readable (and pre-installed on Macs, just open up the Terminal application and enter "python3").

You can copy and paste the below code into a Python script (or Python commandline) and it should print out the same Pulpscript event handler code blob as the above Tic-tac-toe example.

UNDO_LIMIT = 4

for k in range(0,UNDO_LIMIT):
	print(f'on setStateT{k}ToCurrent do')
	print(f'  // log "@setStateT{k}ToCurrent"')
	print(f'  stateT{k}_player_x = playerStateLatest_x')
	print(f'  stateT{k}_player_y = playerStateLatest_y')
	for i in range(0,3):
	    for j in range(0,3):
    		print(f'  stateT{k}_x_{i}_y_{j} = name {i},{j}')
	print(f'end')
	print()

for k in range(0,UNDO_LIMIT):
    print(f'on setCurrentToT{k} do')
    print(f'  // log "@setCurrentToT{k}"')
    print(f'  goto stateT{k}_player_x,stateT{k}_player_y')
    print(f'  playerStateLatest_x = stateT{k}_player_x')
    print(f'  playerStateLatest_y = stateT{k}_player_y')
    for i in range(0,3):
        for j in range(0,3):
            print(f'    tell {i},{j} to')
            print(f'      swap stateT{k}_x_{i}_y_{j}')
            print(f'    end')
    print(f'end')
    print()
    


Note that depending on what language you use, you may need to "escape" certain characters. For example in Python formatted strings, the brackets {} in the strings are used to insert Python code (the variables i, j, k in this case). So if I wanted to print literal bracket characters in my result Pulpscript (maybe I want to log some Pulpscript variable's value), in the Python code I'd have to use double brackets like {{this}}.

It might take a couple tries to get the formatting just how you like it (tabs and spaces and indentation) but with this we can generate the necessary Pulpscript all the way up to whatever UNDO_LIMIT we want to set. Just remember that your Pulpscript code size (and result .json and .pdx files) will grow as well!

Finally, these numbered events can be called from somewhere else using a variable to insert a number (like Shaun describes here).

// player script, with so much omitted for brevity that this is almost pseudocode

on update do
	playerStateLatest_x = event.px
	playerStateLatest_y = event.py
	call "setStateT{timeVar}ToCurrent"
    timeVar++
end

on cancel do
	log "undoing now"
    timeVar--
        if canUndo==1 then
			call "setCurrentToT{timeVar}"
        else
            log "can't undo any further!"
end

(There was a lot more code needed for the full undo implementation, but that's a story for another time. EDIT: it is now another time, see here)

That's all I've got for now, hope someone found this helpful. Feel free to reply with your own workflow tips!

2 Likes

There are a variety of reasons why the method described above is the better and right way to do this, but I want to add a jury-rigged method for those who, whethere due to lack of knowledge or other reason, can't get the procedural generation methods above to work: Excel.

Any equation you can run to create an excel sheet, you can run within excel to output code. You just include all of the necessary spaces within the text cells, then copy and paste the code into a text editor. Ctrl+H to replace tabs (^t to search for tabs in word, or copy and paste a tab to find tabs in notepad) with nothing to remove all the tabs. By default excel adds tabs between each cell when exporting via copy-paste.

I use this a lot when creating code that has a variable that increases by 1 each time, i.e.:

on writevalue1 do
value1 = inputvalue
end

on writevalue2 do
value2 = inputvalue
end

on writevalue3 do
//...

I found myself copy-pasting between Pulp and VS Code a lot to take advantage of its find and replace functionality. Found it difficult to read so I went ahead and developed an unofficial syntax highlighter: PulpScript Syntax - Visual Studio Marketplace

Feel free to DM if you find anything wrong/missing as this is my first extension. Briefly looked into porting this to Nova but I don't currently have plans of doing that

2 Likes