Skip to content

FFL FAQ

Richard Kettering edited this page Nov 3, 2017 · 12 revisions

FFL FAQ

This FAQ covers the sorts of questions that veteran programmers will often have about Anura and the FFL programming language. It might be a bit confusing if you're not an experienced programmer, but most of the answers are valuable to everyone.

Q: So what is FFL, anyway?
A: FFL stands for "Frogatto Formula Language". It's the scripting language for Anura, the engine our game runs on.

It's named in honor of Frogatto because originally, Anura was built-as a single-purpose engine intended to run only the game Frogatto, and nothing else. Because FFL proved to be a flexible and powerful language, and because the engine was in practice quite general purpose, we decided early on to change our plans. We shifted to a design where Frogatto would be merely a "first showcase game" for the engine, and the engine was enhanced a bit to become truly general purpose, allowing 'modules' containing all the scripting and assets for individual games to simply be dropped into a subfolder. We then gave the engine a unique name (Anura, a scientific term for Amphibians), and we split the code for the game and engine apart. The name FFL, though, was too much work to change (it had tons of references in our documentation and code), and we decided we'd leave it that way in honor of its origin.

Q: Why should I use Anura to write my game, anyway?
A: The biggest reason is FFL. Anura itself is a pretty robust engine with a broad-feature base to recommend it (including some unusually good sound support, a live-editor, and unusually good support for pixel-art), but FFL is probably the most important asset.

FFL makes it much easier to manage enormous codebases. If you're pursuing any serious, long-term project, you will unavoidably write a ton of code. There's no way around it - even if you're really economical about fighting code-bloat, there's a point past which you can't simplify code without simplifying your game, so if you're doing anything ambitious and complicated, you're gonna have a lot of complicated code.

FFL provides some extremely nice safety guarantees as your project matures. For example - in a successful project, you'll often need to change a core function you wrote two years ago; say, you need to add a parameter. Maybe you're adding something new and need to pass along more info, maybe you're correcting a mistake, who knows - but the need to do it is common. You'd like to change it, but… the value it returns is used in several dozen places, which are each referenced numerous times. And then those are called by other functions!

Most of us early in our programming careers have tried to tough that out, and we've learned the hard way that it's a disaster to try and do that. Sure, you can put on your mud boots and start shoveling, and you'll probably seem to finish the job in a few nights. … but then the bugs start coming in. And they just keep coming. You end up spending a lot more time fixing the bugs after your change, than it ever took to do the change itself. And the bigger your project gets, the worse this gets, until it reaches the point where as a wiser, sadder veteran programmer, you take a long hard look at that core function, the bitter price you'll have to pay if you change it, and you decide that as nice as that enhancement or fixup would be, it's just not worth it. So you're stuck with it.

FFL is a "force multiplier"; it's intended to let you work with much larger codebases, and not spend all your time fixing bugs. One of the biggest things that does this is our "strict type system". In Anura, when you change the parameters of the old function, the engine will tell you all the places that relied on the old set of parameters. And, in turn, all the places that relied upon those places. And it will do so without making you actually run the code to find the error, which is hideously useful when "what might have broken" could potentially be … "anything in the whole game". How do you test that, manually? Well, you play through the whole game, over and over, thousands and thousands of times over the lifetime of your project. As a veteran, you'll probably agree that anything that cuts down on that is priceless.

These features are not a silver bullet, they're not the Second Coming; you'll still have lots of bugs, and you'll still have to do lots of manual testing. But they're a huge improvement. They might shave months or years off a project.

Q: I'm writing some expressions in FFL, I keep quitting the game and reloading after updating the file, and this is a pain since it keeps crashing with an assertion error if I get something slightly wrong.
A: There are a few solutions.

  • Press ctrl+d in game to bring up the debug console, you can paste (ctrl+v) and copy (ctrl+c) expressions to and from there. The assertions don't cause the game to crash, so you can test till you're happy the code works the way you intend.
  • The game has a built-in text editor which causes updates to the behaviour in real time. Press ctrl+e for the in-game editor. There is a "code" button at the top brings up the code for the currently selected object.
  • If you're running the game from the command-line, try running with the option --edit-and-continue. (Example: ./anura --level=to-nenes-house.cfg --edit-and-continue.) This will bring up the code editor for the object that is causing the crash.

Q: How do I make a variable? Seems like a lot of stuff is read-only.
A: Yeah, being read-only (aka "immutable") is one of those "force multipliers" we were talking about. You'll want to make a custom object property, and then store the information in it. (There are also a bunch of built-in properties for pre-existing things like position, scale, and so on.)

Q: When I try to set the value in an array it doesn't work. e.g set(some_list[1], 5)
A: Lists items are immutable, you need to set the whole list, set(some_list, some_list[0] + [5] +some_list[2:]).

Q: I want to write several lines of FFL code that each do different things, and have them all run.
A: The way you run multiple lines of code in FFL, is to use a list (you can also call this an array if you'd like, but we call it list in our docs). To make a list, take all of your FFL statements (those lines that each do something separate), and enclose them in square brackets ([]). If this code is in some event (described elsewhere in this FAQ), then when that event fires, all the code in your list will run.

Q: Okay, that's nice, but I want this list of code to have each line happen separately, one by one. I can't make sense of what order they're happening in. A: This may sound a bit wild, but they're trying to all happen at once. This is one of those 'force multiplier' things we mentioned - it's called 'pure functional' programming. The idea is that they all run themselves in a way that keeps them from screwing each other up. For example, let's say you've got one command that's supposed to deal damage to an enemy in a game. Then you've got a second command that 'knocks back' that enemy. Well... what happens if that first command does so much damage that the enemy 'dies' and gets deleted by the game? In most engines that's a crash.

Turns out that a pretty bulletproof way to solve this is to handle each step separately - in the sense that each command acts like it's in its own dimension, free to figure out the results of what it's going to do (including deleting other stuff) without immediately deleting that other stuff. Instead, it just tallies up a list of plans, such as "yeah, I'm gonna need to delete that after the other thing is done calculating its results." This way, you know that every command's ingredients don't get screwed with or deleted before it has a chance to read them. Then at the end of that graphical frame (or really, 'internal, physics-calculation' frame), they all get applied at once.

Q: Well, great, but I really need them to happen separately, one by one. A: We definitely do support that. You don't want to do that most of the time, but sometimes, it's critical. The way to change this behavior is to still make a list (one of these [] guys) of commands, but to separate them by semicolons instead of commas. The semicolons signal to the engine that it should figure out the results of the command(s) that came before it, and then change the game state accordingly, before feeding the 'state of the game' in as the ingredients for the next command.

For example, this means that [set(custom_storage.x,5), debug(custom_storage.x)] will print null. However, [set(custom_storage.x,5); debug(custom_storage.x)] will display 5 since it uses a semicolon instead.

Q: So I read up on what 'pure functional' programming means, and it seems like FFL is breaking those rules? All the commands like set(), debug() etc clearly mutate state!
A: They do change the state but they don't mutate the state when that FFL is executing. What happens internally is that the FFL is executed and it returns a command object/series of command objects. When these command objects are executed they modify state. So FFL when it is executed doesn't modify state, but it does return commands that subsequently do. THAT SAID, the ; operator is basically the anti-functional operator; it says "Ok, before I evaluate this following command, evaluate and run this other command first."

Q: What are all these on_xxxx things?
A: Those are event handlers called in response to the given event. The value of them is the FFL to be executed in response. e.g on_process:"debug('A statement')". on_process is called at the tick frequency that the game runs (usually 60 times a second). There are lots of different handlers.

Q: So how do I print out debug information?
A: You can print a variable with debug(x), where x is whatever you are curious about. However, since debug() returns an FFL command, it can't be used inside a calculation which relies on x. If you need the return value of x, you can use dump(x) which does the same thing but returns x instead of an FFL command.

Q: I see the word context used as a variable in some files, what's this for?
A: It is a remnant from the way map/etc used to work and is no longer necessary. The scope of execution inside these functions limits the namespace making context unnecessary.

Q: How do I define my own functions?
A: Those usually get defined as a custom object property. (Basically, in the properties section, you could declare double: def(int x) -> int x*2.)

Q: How do I do a loop in FFL? How do I iterate over lists? What about foreach?
A: map([1,3,5], value*2) yields [2,6,10]. You can also fold a list, like fold([1,2,3], a+b) which yields 6. You could also write a recursive function - but this is hardly ever needed.

Q: What are map, filter, find, fold and how do I use them?
A:

  • map: Applies the given function to each element in a list or map object.
  • filter: Searches through a list and returns a new list containing only those elements matching the selection criteria.
  • find: Searches through a list and returns a single element matching the search criteria.
  • fold: Applies the given function to the first two elements on the list, then to that result and each subsequent element.
    e.g.

--> map(['a', 'b', 'c', 'd'], value+'fgh') //adds the string 'fgh' to each element in the list
['afgh', 'bfgh', 'cfgh', 'dfgh']

--> filter([1,2,3,4,5,6], value % 2) //selects odd values in the list
[1,3,5]

--> find([{id:'a',a:1},{id:'b',b:2},{id:'c',c:3},], value.id = 'b' ) //returns the map item from the list having an id attribute of 'b'
{b: 2, id: 'b'}

--> fold([true,false,false,false], a or b) //returns a value computed by doing an inclusive or on all the elements in the list.
true

Oh and from the example of summing lists, this is how you would do it in FFL: zip([1,2,3],[4,5,6],a+b) -- easy eh?

Q: Wait, what's all this stuff with value and a b in those functions?
A: If you're going to customize how those functions combine things (like telling the zip function that you want to subtract "the thing in the second list" from "the thing in the first list", you need some nouns to describe the actors in the scene, and some verbs to describe what they do (addition or subtraction, in this case). It might be possible to do some simple operations like addition and subtraction by just naming the operation you're doing, and assuming it's always "one thing from the first list" doing the operation you picked to "one thing from the second list".

But if you want to do anything more complicated from that, it's impossible unless we actually give "one thing from the first list" a name. Without a name, describing what you want to do for anything more complex than a two-element operation is straight-up impossible. And since it's not much more to type, we keep the rules simple by just insisting that you do it all the time. So we call them a and b in functions like zip where we're working with two lists, and on functions like map where we're working on just one list, we call the current item value.

(It's worth pointing out that on fold, which you could use to add up a list of numbers to get the total, b means "the current sum" (aka the "accumulated value") instead of being "the current item from the second list".

It's also worth pointing out that when map is used on a map object there is also the symbol key which represents not "the value stored in the current field you're working on", but instead the "name of the field" - also known as a "key", hence the name.)

For an example of that key thing: this function will recurse over a map, and turn it into a list of maps, each map containing a single key:value pair.

--> map({a:1, b:2, c:3}, {(key):value})
[{a:1}, {b:2}, {c:3}]

Q: Wait! The above example looks fishy, why is there a pair of parenthesis around the key symbol?
A: Because of how maps are constructed, i.e. key values can be specified as strings when constructing a map with or without quotation marks. So 'id' is equivalent to id. The downside of this is that when using a key in this way inside a map statement the key symbol is treated as a string literal. Thus to protect it we need to enclose it in parenthesis to indicate that we want the symbol value.

Q: I noticed that FFL is surrounded by double quotes in the event handlers and uses single quotes for strings. What if I want to use " or ' inside a string?
A: Inside an FFL statement you can use the q operator to change from using single quotes to some other symbol to delimit strings. so qThis is a string is treated as being equivalent to 'This is a string', except that it frees you up to use single quotes inside your string. The q operator can used most symbols as delimiters for this purpose. Brackets are matched and can be nested.

Q: Does FFL support tail recursion?
A: Yes, so long as you structure your recursive function so that the calling function simply returns the value it gets from the called function. Compare these two functions the first is not tail recursive the second is. The difference is the accumulator we pass, in the first case we do the multiplication on returning back up the call stack. In the second case we do the evaluation going down the call stack, meaning that each returned value is simple, thus the FFL interpreter can optimize removing the tail recursion.

def fact(n)if(n=0,1,fact(n-1)*n)
fact(10) = 3628800

def fact_tail(n,x) if(n=0,x,fact_tail(n-1,n*x))
fact_tail(10,1) = 3628800

There is also a special syntax to ensure recursion. From data-types, a find-index function:

def my_index(ls, item, n)
    base ls = []: -1       #case where we've got to the end of the list and nothing found#
    base ls[0] = item: n   #case where we've found a match#
    recursive: my_index(ls[1:], item, n+1)  #recursive case: no match -- search next item#

Q: How do I do if … else in FFL? How do I do if…else if…else…?
A1: if(condition, statement_if_true, statement_if_false)
A2: if(condition1, statement1_if_true, if(condition2, statement2_if_true, else_condition))
By nesting the basic if() statement we can chain together if/else logic.

Q: What is where for?
A: It's used simplify statements that might be used in multiple places in an expression, or just to keep the logic clear. It can be thought of as macro expansion, in simple terms, but it's more expressive than a straight macro expansion, in that where gets evaluated before being substituted into the expression.
e.g.

[set(vars.button_left, b), debug('Left button assigned to ', b), set(vars.buttons, vars.buttons+b)]
where b = find(button_list, value.id = 'left')

You can define multiple items using where by separating them with a comma.

--> set(custom_storage.x, {(a):b}) where a='id', b=5
--> custom_storage.x
{'id': 5}

Basically we only want to do the find once, then use it in three different places in the expression. But we can also define anonymous functions with where.

--> def fact(n) factt(n,1) where factt = def(n,acc) if(n<=0,acc,recurse(n-1,acc*n))
--> fact(10)
3628800

Q: recurse? tell me, tell me.
A: recurse basically lets you recursively call on the current anonymous function. See above for the factorial example using recurse.

Not specifically FFL questions, but more FSON.

Q: So I looked at some of game files, but they didn't seem to make much sense.
A: And your question was?

Q: Okay, smart alec, what's inside the files then?
A: All the game files are FSON, which is very similar to JSON. It has a few extensions to support comments. Strictly speaking FSON is also a subset of FFL. On the most basic level all the document is, is a series of key:value pairs, split into lists and maps. These map directly into the primitives used in FFL, which supports maps and lists. Maps are also often referred to as hash-maps, dictionaries, or associative arrays. Generally speaking they lets you search very quickly for an indexed item (O(log(n) time -- but this implementation dependent). The basic syntax for a map in an FSON file is {a:1, b:2, c:3}. Keys are generally string values, and may be unquoted - the parser treats id as synonymous with 'id'. The value portion doesn't have to be a simple integer either. It can be another map, or a list, string, or number. Thus complex data can be built up. Lists are simply comma delimited series of items. For example, [{a:1},{b:2},{c:3}] is a list of maps.

Clone this wiki locally
You can’t perform that action at this time.