Lobster is a game programming language. Unlike other game making systems that focus on an engine/editor that happens to be able to call out to a scripting language, Lobster is a general purpose stand-alone programming language that comes with a built-in library suitable for making games and other graphical things. It is therefore not very suitable for non-programmers.
It's also meant to be portable (mostly courtesy of OpenGL/SDL/Freetype), allowing your games to be run on Windows, Mac OS X, iOS, Linux, and Android (in that order of maturity, currently).
Features have been picked for their suitability in a game programming language, and in particular to make code terse, quick to write and refactor. It is meant to not hold you back to get an idea going quickly. It is quite the opposite of a robust enterprise language.
let's start with syntax and blocks:
function find(xs, fun): for(xs) x, i: if(fun(x)): return i return -1 r := 2 i := find([ 1, 2, 3 ]): _ > r
We can learn a lot about the language from this tiny example:
findis a function that takes a vector and a function as argument, and returns the index of the first element for which that function returns true, or -1 if none.
for-if-returncould also have been written on a single line.
:. The body may either follow directly, or start a new indentation block on the next line. Additionally, if you don't feel like declaring arguments, you may use variable names starting with an
_inside the body that are automatically declared.
iflook like language features, but they are not. They are part of the built-in library, and are not a required part of Lobster, and have no special syntactical status compared to
find. Any such functions you add will work with the same syntax.
findget specialized to work on whatever arguments they are called with, in this case a list of ints, and a specific lambda. Specialization not only increases the range of code type inference can handle, it allows the compiler to optimize this particular case as if you had hard-coded the loop (much like C++ templates).
r. This is essential to utilize the full potential of blocks.
2at the end of this (the index of element
3). It does not clash with the other
ibecause of lexical scoping. Here
=means assignment, and
:=means define & assign.
find, not from the enclosing function (which would be the block passed to
if). In fact, it can be made to return from any named function, which makes it powerful enough to implement exception handling in Lobster code, instead of part of the language.
findeasy to write and use, you can convert any such functions into coroutines trivially, e.g.
coroutine for(10)creates a coroutine object that yields values 0..9 on demand. Because in lobster coroutines and higher order functions are written in the same way (there is no yield keyword), they are more composable and interchangable.
Types, multimethods, immutables and vector ops:
value point: [ x, y ] value circle: point [ radius ] value ray: point [ dir ] function intersect(p:point, c:circle): magnitude(p - c) < c.radius function intersect(r:ray, c:circle): ... function intersect(c1:circle, c2:circle): ... ... assert(intersect([ 1, 1 ]:point, [ 2, 2, 2 ]:circle))
What we learn here:
struct, where the former means the object is immutable: its fields may not be assigned to after construction. This enforces more functional style programming for objects which can be seen as unit things (like points and vectors).
intersectrequires double dispatch, i.e. 2 levels of methods). If two functions apply to a certain set of arguments, the most specific one (starting from the first argument) is picked. If such an ordering can't be determined at compile time, that is a compile time error. If no functions apply at runtime, that's a runtime error (which you can avoid with a catch-all default function version with no types).
:, also for single functions. These are still runtime errors if not matched, not compile time. They are for documentation purposes and to force type errors to happen earlier than without. You can even specify the type with
::, which allows you direct access to all members of the type, so you can write
Enough of dry programming language stuff, how do we draw?
include "vec.lobster" include "color.lobster" directions := [ xy_0, xy_x, xy_y ] function sierpinski(depth): if(depth): gl_scale(0.5): for(directions) d: gl_translate(d): sierpinski(depth - 1) else: gl_polygon(directions) fatal(gl_window("sierpinski", 512, 512)) while(gl_frame()): if(gl_wentdown("escape")): return gl_clear(color_black) gl_scale(gl_windowsize()) sierpinski(6)
What do we see:
gl_window, this creates the window and sets up OpenGL basics. This can theoretically fail which will return us an error string, but here for the example we're lazy.
gl_frametakes care not only of frame swapping, but updating input etc as well
gl_scalevecallows us to scale all rendering by specifying the unit size (compared to the previous scale, which by default is pixel size). Using the current window size thus gets us a canvas with a resolution of 1.0 x 1.0 which is convenient for the algorithm we're about to run
vec_0is a vector of all zeroes).
To see more about the builtin functionality of Lobster (graphics or otherwise), check out the builtin functions reference (this particular file may be out of date, it can be regenerated by the running “lobster -r”). You can also check out draft version of the full Lobster documentation, in particular the Language Reference.
Most recent version of everything is on GitHub.
And for fun here's a Minecraft clone in lobster whose code fits inside a single screenshot.
And finally, as an example of the extreme economy of code you can get to using the compositionality of higher order functions judiciously, the code below implements the A* algorithm for any kind of search, with specialized versions for nodes in 2D/3D/nD space, 2D grids, and GOAP (goal oriented action planning) all in a tiny amount of code, with no code duplication. Yes, readability suffers, but if you've ever implemented these algorithms (including GOAP), you probably realize this is quite an extreme level of factoring:
// A* search functionality include "std.lobster" include "vec.lobster" struct astar_node: [ G:float, H:float, F:float, previous, state, delta, open:int, closed:int ] function new_astar_node(state, h:float): [ 0.0, h, h, nil, state, nil, false, false ]:astar_node function astar_clear(n::astar_node): open = closed = false previous = nil // the generic version searches any kind of graph in any kind of search space, use specialized versions below function astar_generic(startnode, endcondition, generatenewstates, heuristic): openlist := [ startnode ] n := startnode | nil while(n & !endcondition(n)): openlist.removeobj(n) n.closed = true generatenewstates(n) delta, cost, nn: if(!nn.closed): G := n.G + cost if((!nn.open & openlist.push(nn)) | G < nn.G): nn.open = true nn.delta = delta nn.previous = n nn.H = heuristic(nn.state) nn.G = G nn.F = G + nn.H n = nil for(openlist) c: if(!n | c.F < n.F | (c.F == n.F & c.H < n.H)): n = c path :=  while(n): path.push(n) n = n.previous path // specialized to a graph in 2D or 3D space (assumes pre existing nodes), usage: // - create a graph out of nodes inherited from astar_node above // - costf must compute the cost of traversal between 2 nodes // - neighbors generates adjacent nodes // - returns a list of nodes from end to start inclusive, or empty list if no path function astar_graph(startnode, endnode, costf, distancef, neighbors): astar_generic(startnode) n: n == endnode generatenewstates n, f: neighbors(n) nn: cost := costf(n, nn) if(cost > 0): f(nn.state - n.state, cost, nn) heuristic state: distancef(state - endnode.state) function astar_space(startnode, endnode, costf, neighbors): astar_graph(startnode, endnode, costf, function(v): magnitude(v), neighbors) // specialized to a 2D grid (specialized case of a graph) function astar_2dgrid(isocta, gridsize, startnode, endnode, getnode, costf): directions := [ [ -1, 0 ]:xy, [ 1, 0 ]:xy, [ 0, -1 ]:xy, [ 0, 1 ]:xy ] function astar_distance(distancef): astar_graph(startnode, endnode, costf, distancef) n, f: for(directions) delta: np := n.state + delta if(np.inrange2d(gridsize, xy_0)): f(getnode(np)) if(isocta): directions = directions.append([ [ -1, -1 ]:xy, [ 1, 1 ]:xy, [ 1, -1 ]:xy, [ -1, 1 ]:xy ]) astar_distance() v: x := abs(v.x) y := abs(v.y) big := max(x, y) small := min(x, y) sqrt(2) * small + big - small else: astar_distance() v: abs(v.x) + abs(v.y) // specialized to do GOAP (nodes created on the fly) function goapf(state:[int]) :== int value goapaction: [ name:string, precondition:function goapf, effect:function goapf ] function astar_goap(goapactions:[goapaction], initstate:[int], heuristic, endcondition): existingnodes := [ new_astar_node(initstate, heuristic(initstate)) ] astar_generic(existingnodes): endcondition(_.state) generatenewstates n, f: goapactions.for() act: if(act.precondition(n.state)): nstate := n.state.copy act.effect(nstate) i := existingnodes.find(): equal(_.state, nstate) nn := i >= 0 & existingnodes[i] | new_astar_node(nstate, 0.0) f(act, 1, nn) heuristic: heuristic(_)
Also note that while the structs contain some types, the functions contain very little of them, showing the power of the type inference algorithm.
Probably not the best example to sell a language.. it is possible to write simple looking code in it too, honest :)