Ink ( for experienced coders)

nice90sguy

Out To Lunch
Joined
May 15, 2022
Posts
1,591
I started learning Ink recently, prompted by the new Story Game feature here. I've been coding most of my life, in literally dozens of languages, and learning a new language is always a fun challenge for me.

This thread is a blog, which I'll update as I learn stuff during development of my game.

There will be some useful snippets posted here, but this is NOT a howto, or a tutorial.
 
Last edited:
Implementing gmtime in Ink.

The Un*x library function gmtime is implemented in C, as are most of its libraries.

To implement this in Ink, I started with the code from this StackOverflow answer: https://stackoverflow.com/a/15649301/1057849

C:
struct tm *
gmtime(register const time_t *timer)
{
        static struct tm br_time;
        register struct tm *timep = &br_time;
        time_t time = *timer;
        register unsigned long dayclock, dayno;
        int year = EPOCH_YR;

        dayclock = (unsigned long)time % SECS_DAY;
        dayno = (unsigned long)time / SECS_DAY;

        timep->tm_sec = dayclock % 60;
        timep->tm_min = (dayclock % 3600) / 60;
        timep->tm_hour = dayclock / 3600;
        timep->tm_wday = (dayno + 4) % 7;       /* day 0 was a thursday */
        while (dayno >= YEARSIZE(year)) {
                dayno -= YEARSIZE(year);
                year++;
        }
        timep->tm_year = year - YEAR0;
        timep->tm_yday = dayno;
        timep->tm_mon = 0;
        while (dayno >= _ytab[LEAPYEAR(year)][timep->tm_mon]) {
                dayno -= _ytab[LEAPYEAR(year)][timep->tm_mon];
                timep->tm_mon++;
        }
        timep->tm_mday = dayno + 1;
        timep->tm_isdst = 0;

        return timep;
}

It makes use of a few constants and macros:

C:
#define YEAR0           1900                    /* the first year */
#define EPOCH_YR        1970            /* EPOCH = Jan 1 1970 00:00:00 */
#define SECS_DAY        (24L * 60L * 60L)
#define LEAPYEAR(year)  (!((year) % 4) && (((year) % 100) || !((year) % 400)))
#define YEARSIZE(year)  (LEAPYEAR(year) ? 366 : 365)

This code is pretty straightforward, being mainly integer arithmetic, table lookup and a couple of while-loops. Should be easy to convert, right?

Ink's arithmetic is pretty similar to C's integer arithmetic: So, given an integer value, the division operator produces an integer result (i.e. no fractions), so
Code:
Ten divided by three = {10 / 3}
produces the text
"Ten divided by three = 3".

Modulo arithmetic and operator precedence also align with C, so basically C integer expressions can pretty much be copied as-is.

Where Ink is really different from C, and also from pretty much any other language, is with flow of control and conditionals. But, with a little work, most of C's control flow and conditional logic can be implemented in Ink.

Lets start with the YEARSIZE macro above:

In Ink, this can be implemented with a function:

Code:
=== function YEARSIZE(year)
     { LEAPYEAR(year) == 1:
        ~ return 366
     -   else:
        ~ return 365
    }
and the LEAPYEAR macro, like this:
Code:
// returns 1 if leap year, 0 otherwise
=== function LEAPYEAR(year)
~ temp is_leap_year = (!(year % 4) && ((year % 100) || !(year % 400)))
    { is_leap_year:
        ~ return 1
    - else:
        ~ return 0
    }

Notice how the expression for is_leap_year in the Ink function matches the C macro exactly, but, unlike C, where true is 1 and false is 0, you need to explicitly convert the boolean to an integer.*

*Actually that's not true, but it's safer to code it as I have done, because I don't think Ink will always guarantee that 1 and true are interchangeable
 
Last edited:
Implementing gmtime in Ink (contd)

Now, how about C's "while loops"?

This is one way of doing it (the way I ended up doing it):

If the C is this:
C:
while (some_condition) {
    do_stuff;
}

Then the equivalent Ink is
Code:
= loop_label1
    { !(some_condition):
      -> loop_label1_break
     - else:
      ~ do_stuff
     -> loop_label1
      }
= loop_label1_break


The first while loop in the C gmtime function then becomes:

Code:
= gmtime_loop_1
    ~ temp yearsize = YEARSIZE()
    { dayno < yearsize:
      -> gmtime_loop_1_break
     - else:
      ~ dayno -= yearsize
      ~ year++
      -> gmtime_loop_1
      }
= gmtime_loop_1_break
 
Last edited:
Implementing gmtime in Ink (contd)

Table lookups are a pain in Ink -- or maybe I'm missing something.

I started looking at LISTs for these, until I discovered that they're not really suitable at all -- although they have their uses for game management (which I'll talk about in another post).
As the lookup tables used in gmtime.c are small (they're tables of the number of days in each month for leap and non-leap years), I made a function that used Ink's "switch" statements to implement the table lookup, and also simplified the two tables into a single lookup, as the two tables are the same except for the entry for February:

Code:
=== function month_duration_days()
    ~ temp add_day_to_feb = LEAPYEAR()
  
    { month:
        - 1: ~ return 31
        - 2: ~ return 28 + add_day_to_feb
        - 3: ~ return 31
        - 4: ~ return 30
        - 5: ~ return 31
        - 6: ~ return 30
        - 7: ~ return 31
        - 8: ~ return 31
        - 9: ~ return 30 
        - 10: ~ return 31
        - 11: ~ return 30
        - 12: ~ return 31
      }

Notice that this function doesn't have any arguments -- the final implementation of the gmtime function uses global variables for "year", "month" etc, which simplifes the code, and other parts of the game code need to access them.
 
Last edited:
Implementing gmtime in Ink (contd)

Putting all of this together, here's the complete code for gmtime, which I implemented as a tunnel, rather than a function.
I'll talk about tunnels in another post.

I also added a few more functions for rendering month and day names, and formatting the numbers nicely.

You can check it using https://www.epochconverter.com/.

Code:
/*
Copyright (c) 2024 nice90sguy@gmail.com
Portions copied right (!) from  Un*x gmtime.c
*/
VAR dayno = 0
VAR year = 0
VAR epoch_time = 0
VAR month = 1
VAR day = 0
VAR tm_sec = 0
VAR tm_min = 0
VAR tm_hour = 0
VAR tm_wday = 0
CONST SECS_DAY = 86400
CONST EPOCH_YR = 1970
CONST YEAR0 = 1900

/*
Using the global var "epoch_time", which should be a unix timestamp (seconds since Jan 01 , 1970), update these global variables (which are all integer values):

   year - The year
   month - The month (Jan is 1)
   day - day of the month
   tm_wday - The day of the week (Sun is 0, Sat is 6)
   tm_hour - The hour (0 - 23)
   tm_min - Minutes past the hour
   tm_sec - Seconds past the minute
*/
=== gmtime

    ~ year = EPOCH_YR
    ~ temp dayclock = epoch_time % SECS_DAY
    ~ dayno = epoch_time / SECS_DAY
    ~ tm_sec = dayclock % 60
    ~ tm_min = (dayclock % 3600) / 60
    ~ tm_hour = dayclock / 3600
    ~ tm_wday = (dayno + 4) % 7
    -> gmtime_loop_1

= gmtime_loop_1
    ~ temp yearsize = YEARSIZE()
    { dayno < yearsize:
      -> gmtime_loop_1_break
     - else:
      ~ dayno -= yearsize
      ~ year++
      -> gmtime_loop_1
      }

= gmtime_loop_1_break
    ~ month = 1
    -> gmtime_loop_2

= gmtime_loop_2
  ~ temp month_length = month_duration_days()
    { dayno < month_length:
     -> gmtime_loop_2_break
     - else:
        ~ dayno -= month_length
        ~ month++
     -> gmtime_loop_2
     }

= gmtime_loop_2_break
     ~ day = dayno + 1
 
->->

// returns 1 if leap year, 0 otherwise
=== function LEAPYEAR()
    ~ temp is_leap_year = (!(year % 4) && ((year % 100) || !(year % 400)))
    { is_leap_year:
        ~ return 1
    - else:
        ~ return 0
    }
 
=== function YEARSIZE()
     { LEAPYEAR() == 1:
        ~ return 366
     -   else:
        ~ return 365
    }
 
=== function month_duration_days()
    ~ temp add_day_to_feb = LEAPYEAR()
 
    { month:
        - 1: ~ return 31
        - 2: ~ return 28 + add_day_to_feb
        - 3: ~ return 31
        - 4: ~ return 30
        - 5: ~ return 31
        - 6: ~ return 30
        - 7: ~ return 31
        - 8: ~ return 31
        - 9: ~ return 30
        - 10: ~ return 31
        - 11: ~ return 30
        - 12: ~ return 31
      }
 
=== function month_name()
    { month:
        - 1: ~ return "January"
        - 2: ~ return "February"
        - 3: ~ return "March"
        - 4: ~ return "April"
        - 5: ~ return "May"
        - 6: ~ return "June"
        - 7: ~ return "July"
        - 8: ~ return "August"
        - 9: ~ return "September"
        - 10: ~ return "October"
        - 11: ~ return "November"
        - 12: ~ return "December"
      }
 
=== function day_name()
    { tm_wday:
        - 0: ~ return "Sunday"
        - 1: ~ return "Monday"
        - 2: ~ return "Tuesday"
        - 3: ~ return "Wednesday"
        - 4: ~ return "Thursday"
        - 5: ~ return "Friday"
        - 6: ~ return "Saturday"
      }

/*
return an ordinal from a number
e.g. 1 -> "1st"
12 -> "12th"
33 -> "33rd"
...etc
*/
=== function ord(num)
{ num >= 10 && num <= 20:
    ~ return num + "th"
}

{ num % 10:
 - 0: ~ return num + "th"
 - 1: ~ return num + "st"
 - 2: ~ return num + "nd"
 - 3: ~ return num + "rd"
 - else: ~ return num + "th"
 }

// Add leading zeros for day, month, hour etc if < 10
=== function l0(num)
    { num < 10:
        ~ return "0"+num
    - else:
        ~ return num
    }

Here's the code you can use to test it:

Code:
~ epoch_time = 1720872518 // 2024-07-13 12:08:38
 
   -> gmtime ->


    Date: <b>{day_name()}, {month_name()} {ord(day)} {year}
    Time: <b>{l0(tm_hour)}:{l0(tm_min)}:{l0(tm_sec)}

which should render as:

Date: Saturday, July 13th 2024
Time: 12:08:38
 
Last edited:
Cheatcodes and passwords

Lots of games have "secret" areas you can only enter if you've obtained a cheat code. Or perhaps you want to restrict your game only to users who've you've given a password to.

Using choices, you can do this easily:
Here's how you can create a passcode with 625 possible values:

Code:
-> enter_password ->
-> main_game

CONST password_length = 4
=== enter_password
VAR asterisks = ""
Enter your password ({password_length} characters):
-> next_letter

// Valid passwords, comma separated
CONST passwords = "EEEE,ABCB,AEAB"
VAR password = ""

VAR i = 0
= next_letter
+ [A]
~ password += "A"
+ [B]
~ password += "B"
+ [C]
~ password += "C"
+ [D]
~ password += "D"
+ [E]
~ password += "E"
-
    ~ i++
    ~ asterisks += "*"
    {asterisks}
    {i < password_length:

        -> next_letter
    - else:
        -> check_password
        }

= check_password
{passwords ? password:
    You may enter!
    - else:
    Incorrect password
    -> END
    }
->->
 
Last edited:
Tunnels

Tunnels are basically subroutines, and I've found they're the key to writing a game, for me.

Here's a tunnel I use all the time, to break up my story into small chunks of text:


Code:
// "Continue" prompt
=== cont
+ [<i>Continue...</i>]
-
->->

I add
Code:
-> cont ->
every couple of hundred words or so in the narrative parts.

Notice two important things:

I'm using a '+' rather than a '*' for the (single) user choice, because the tunnel is called repeatedly.
And I put the italicised "Continue" in square brackets to stop it being emitted into the story text, because I don't want the word to appear in the game every time I call it.
 
Include Files

Whether or not Literotica games will support include files (I doubt it will), for developing a non-trivial game in Ink, include files are vital.
The order of includes doesn't matter, so you can declare VARs anywhere, not necessarily at the start of your game, and they get added to the global namespace (and pollute it).

In Inky, use the + Add new include button at the bottom left.

When I eventually upload the game to Literotica, I'll have to get rid of them and copy/paste all the include files into one.
 
I was thinking to write something like your cheat code as a way for users to type in a character name, but it would a be clumsy mechanism indeed.
 
I was thinking to write something like your cheat code as a way for users to type in a character name, but it would a be clumsy mechanism indeed.
I though about it too. I'm setting a limit of 5 items per menu, for aesthetic reasons.

Another way to do it is to have two lists of male and female names, and assign one at random to the player (using the tilde (~) "shuffle" operator on the list.
 
Using loops in functions

gmtime was implemented as a tunnel in This Post. That's because the way I implemented the while loops used stitches and redirects , which aren't available in Ink functions.

But all loops in any programming language that allows recursion (like Ink), can always be done using tail recursion instead. This trick is as very old. The principle is to replace

C:
int i = 0;
// do stuff 10 times
foo ()
{
    while (i < 10) {
        dostuff;
        i++;
    }
    return;
}

With

C:
int i = 0;
// do stuff 10 times
foo()
{
    if (i == 10) {
        return;
    }
    dostuff;
    i++;
    foo();
}

Below is a modified version of gmtime in Ink, implemented as a function instead, which replaces the two while loops with calls to two tail-recursive functions:


Code:
/*
Copyright (c) 2024 nice90sguy@gmail.com
Portions copied right (!) from  Un*x gmtime.c

See also:
https://stackoverflow.com/questions/15627307/epoch-seconds-to-date-conversion-on-a-limited-embedded-device
This gmtime code has been checked against https://www.epochconverter.com/

   Example:
   ~ epoch_time = 1720872518 // 2024-07-13 12:08:38
 
   ~ gmtime()

    Date: <b>{day_name()}, {month_name()} {ord(day)}
    Time: <b>{l0(tm_hour)}:{l0(tm_min)}:{l0(tm_sec)}
*/
VAR dayno = 0
VAR year = 0
VAR epoch_time = 0
VAR month = 1
VAR day = 0
VAR tm_sec = 0
VAR tm_min = 0
VAR tm_hour = 0
VAR tm_wday = 0
CONST SECS_DAY = 86400
CONST EPOCH_YR = 1970
CONST YEAR0 = 1900

/*

Convert an "epoch_time" t, which should be a unix timestamp (seconds since Jan 01 , 1970), update these global variables (which are all integer values):

   year - The year
   month - The month (Jan is 1)
   day - day of the month
   tm_wday - The day of the week (Sun is 0, Sat is 6)
   tm_hour - The hour (0 - 23)
   tm_min - Minutes past the hour
   tm_sec - Seconds past the minute
 
   returns t.

*/
=== function gmtime(t)
    ~ year = EPOCH_YR
    ~ temp dayclock = t % SECS_DAY
    ~ dayno = t / SECS_DAY
    ~ tm_sec = dayclock % 60
    ~ tm_min = (dayclock % 3600) / 60
    ~ tm_hour = dayclock / 3600
    ~ tm_wday = (dayno + 4) % 7
    ~ gmtime_loop_1_recursion()
    ~ month = 1
    ~ gmtime_loop_2_recursion()
     ~ day = dayno + 1
    ~ return t

=== function gmtime_loop_1_recursion
    ~ temp yearsize = YEARSIZE()
    { dayno < yearsize:
      ~ return
     }

  ~ dayno -= yearsize
  ~ year++
  ~ return gmtime_loop_1_recursion()
 
=== function gmtime_loop_2_recursion

  ~ temp month_length = month_duration_days()
    { dayno < month_length:
        ~ return
    }

    ~ dayno -= month_length
    ~ month++
    ~ return gmtime_loop_2_recursion()
 
Last edited:
Varying Dialog

Here’s a dialog that might occur in a story:

“How much is this dress, it doesn't have a price tag?” I asked.

“A lot,” the store assistant replied. “That’s this year’s Cazzoni.”

“If it was last year’s, I wouldn’t be asking you the price. I would be telling you.”

“I’ll have to find out.”

“So, go and find out”.

“I can’t do that right now, everyone’s on lunch break.”

“Don’t worry, I’ll mind the store while you’re gone,” I said.

“Ok, but-“

“-Don’t worry, I won’t steal anything.”


Each line of the dialog is spoken by either me (the player or MC, whose choices I can make), or the store assistant.

In the game, if I wanted to vary this dialog each time the game is played, without altering the meaning of each line, I could use Ink’s “shuffle” feature:

Code:
"{~How much is|Can you tell me the price of} this{~ dress| blouse| skirt| top||}{~, there's no price tag| -- it's unmarked|, it doesn't have a price tag||}?" I asked.
I could even progress the narrative, making some of the dialog progress sequentially:
Code:
-> rodeo_drive -> rodeo_drive -> rodeo_drive -> END

= rodeo_drive
<i>{||For the third time,} I went {|back} to the store on Rodeo Drive...

{The assistant eyed me up and down, and seemed to decide I was a time-waster.||}

"{~How much is|Can you tell me the price of} this{~ dress| blouse| skirt| top}{~, there's no price tag| -- it's unmarked|, it doesn't have a price tag||}?" I asked the assistant.

"It's ${1500|250|150}," she replied{|| politely}.

{I paid for it with my Gold Amex and flounced out without thanking her.|I paid for it and thanked her.|"Too expensive", I said, walking out.}
->->

This produces:

I went to the store on Rodeo Drive...

The assistant eyed me up and down, and seemed to decide I was a time-waster.

"How much is this dress -- it's unmarked?" I asked the assistant.

"It's $1500," she replied.

I paid for it with my Gold Amex and flounced out without thanking her.

I went back to the store on Rodeo Drive...

"Can you tell me the price of this top, there's no price tag?" I asked the assistant.

"It's $250," she replied.

I paid for it and thanked her.

For the third time, I went back to the store on Rodeo Drive...

"Can you tell me the price of this skirt?" I asked the assistant.

"It's $150," she replied politely.

"Too expensive", I said, walking out.
 
Last edited:
Varying Dialog (contd)

The last example was more than just varying dialog: The narrative changed, along with my relationship with the shop assistant: the first and second time I go into the shop, I buy something, but the third time, I dont. And the assistant starts out being snide, but changes her tune once she's seen I have money.

That's pretty hard to see from the code, and even harder to debug.

The following version, which makes use of LISTs, is more flexible (but there's a lot more to do to really make the dialog dynamic and appropriate to the game state):

Code:
LIST visit = (first),second,last,never_gain
LIST items = (dress=1500), skirt=250, top=150
VAR items_available = ()
~ items_available = LIST_ALL(items)

-> rodeo_drive -> rodeo_drive -> rodeo_drive -> rodeo_drive ->  END

Code:
= rodeo_drive

{ visit == never_again:
    I'm not going back there again.
    ->->
}
<i>Items available: {items_available}

<i>For the {visit} time I went {visit > first: back} to the store on Rodeo Drive...
~ temp item = LIST_MAX(items_available)
I found the most expensive-looking item in the store: A {~blue|red|lovely} {item}{~, which would look great on me||}.
{visit == first:
The assistant eyed me up and down, and seemed to decide I was a time-waster.
}

"{~How much is|Can you tell me the price of} this {item} {~, there's no price tag| -- it's unmarked|, it doesn't have a price tag||}?" I asked the assistant.
~ temp item_price = LIST_VALUE(item)

"That {item}? {~I think ||}it's ${item_price}."



{visit == last:
"Too expensive", I said, walking out.
- else:
    I paid for it with my Gold Amex and flounced out without thanking her.
}
~ items_available -= item
~ visit++

->->

The output from one run of the above is:

Items available: top, skirt, dress

For the first time I went to the store on Rodeo Drive...


I found the most expensive-looking item in the store: A red dress, which would look great on me.

The assistant eyed me up and down, and seemed to decide I was a time-waster.

"Can you tell me the price of this dress ?" I asked the assistant.

"That dress? I think it's $1500."

I paid for it with my Gold Amex and flounced out without thanking her.

Items available: top, skirt

For the second time I went back to the store on Rodeo Drive...


I found the most expensive-looking item in the store: A lovely skirt.

"How much is this skirt -- it's unmarked?" I asked the assistant.

"That skirt? it's $250."

I paid for it with my Gold Amex and flounced out without thanking her.

Items available: top

For the last time I went back to the store on Rodeo Drive...


I found the most expensive-looking item in the store: A blue top.

"How much is this top , there's no price tag?" I asked the assistant.

"That top? it's $150."

"Too expensive", I said, walking out.

I'm not going back there again.
 
Last edited:
Arithmetic Expression Gotcha

Ink's operator precedence is non-standard, and you sometimes need parentheses!

Code:
{ 2000 * 5 / 1000 } // result is 0 (wtf)
{ (2000 * 5) / 1000 } // result is 10 (what you probably meant)
 
This language is absolutely horrifying :oops: If I were to do anything in it, I’d delegate any more complex piece of logic into an external function. Your gmtime implementation would be an example, or the “print number” routine in Ink docs.
Yes, a lot of stuff would be much easier that way, but Lit wouldn't accept anything with external functions, so...

That print number routine is awful. To change it to optionionally start with a capital letter when using it to begina sentence, I had to rewrite that.

To "comma-ify" a number (which I'm doing for displaying money), I wrote this:


Code:
== function comma_ify(n)

~ temp isminus = n < 0
{ isminus:
    ~ n  = -n
    \-
}
{_comma_ify(n,  num_digits(n)-1)}


// Add leading zeros to number if necessary
 === function leading_zeros(n, maxnum_zeros)
    ~ temp num_zeros = maxnum_zeros - num_digits(n)
    {num_zeros <= 0:
        {n}
    - else:
        {repchar("0", num_zeros)}{n}
    }
   

// non-negative numbers only!
== function num_digits(n)
    {n < 10:
        ~ return 1
    }
    ~ return num_digits(n/10)+1
   
== function repchar(char, n)

    {n > 0:
        {char}<>
        {repchar(char, n-1)}<>
    }
   

== function _comma_ify(n, order_of_magnitude)
{ order_of_magnitude < 3:{n % 1000}|{_comma_ify(n/1000, order_of_magnitude-3)},{leading_zeros(n % 1000,3)}}
 
Arithmetic Expression Gotcha

Ink's operator precedence is non-standard, and you sometimes need parentheses!

Code:
{ 2000 * 5 / 1000 } // result is 0 (wtf)
{ (2000 * 5) / 1000 } // result is 10 (what you probably meant)
Yeah, it seems Ink is prioritizing / over * so I guess 0 is to be expected. If you do it like this " 2000 * 5.0 / 1000.0 " you don't need parentheses for the correct result.
 
Last edited:
Yeah, it seems Ink is prioritizing / over * so I guess 0 is to be expected. If you do it like this " 2000 * 5.0 / 1000.0 " you don't need parentheses for the correct result.
Hah. Never noticed that, as I only use integer arithmetic
 
This language is absolutely horrifying :oops: If I were to do anything in it, I’d delegate any more complex piece of logic into an external function. Your gmtime implementation would be an example, or the “print number” routine in Ink docs.
I felt the same way. This language is looking ugly! (no literal 'if' but an 'else' statement and such...)
To date, I prefer yarnspinner syntax (https://docs.yarnspinner.dev/getting-started/writing-in-yarn).

But, hey, it'd probably be too much to ask for several story-engine implementations for literotica. (And then you'd still want an external functions library anyway.)
 
Back
Top