Writing:The Ways of Printing

From Seltanikor
Jump to navigationJump to search

Your First Age

Basic Concepts

Working the Example

The Ways of Printing

Assorted Features

Reference Section

The Scripting Language

Best Practices

Code Samples

Most of what Seltani does is display text, so the text-display code is rather complicated. (Some people would say, over-complicated. I would not disagree with those people.)

The most familiar form of text is the Text property; we’ve seen how these are used for room descriptions and object close-up views. You can put various kinds of formatting in a text property, using square-bracket markup.

However, there are other ways to generate text. The most obvious is a Value property containing a string (that is, a value in quotes or double-quotes). This behaves just like a Text property, except that square brackets have no special meaning. Value properties use Python syntax, so you may use backslashes to escape quotes and other special characters.

You can also write a Code property which contains print() statements. In general, a Text property

You see stuff.

...is equivalent to a Code property:

 print("You see stuff.")

One important distinction: if you name a Text property on a line by itself, or as a link target, it is displayed in the focus window. If you name a Code property this way, it is not focussed; it runs immediately. To set a Code property as the focus, do: setfocus("codepropname").

Again, square brackets have no special meaning in Python string literals. So this will not do what you want:

 print("You see stuff.[$para]Does not produce a new paragraph.")

However, there are function equivalents to all of the special text operators. So you might do this:

 print("You see stuff.")
 print("This is a ")
 print(" paragraph.")

If you print several items in a single line, they are separated by spaces:

 print("You", "see", "stuff.")

You can change or remove this separator by passing a sep argument:

 print("You", "see", "stuff.", sep="/")

(However, see the note below about whitespace. Passing a Python newline as a separator will not generate a visible line break, because Seltani collapses whitespace.)

There’s one more way to generate text: the GenText property. But this is sufficiently complicated to warrant its own section.

A note about Unicode and whitespace

Seltani uses Unicode text throughout. This means that you do not need special escape sequences to generate accented or foreign-language characters. You may type any character you want directly into the world-building interface, and it will appear correctly in the world.

The D’ni alphabet is an exception, because it has no standard Unicode representation. For D’ni characters and digits, use the [[style("dni")]]...[[endstyle("dni")]] interpolation in Text, or style("dni"); endstyle("dni") calls in Code.

Whitespace is collapsed according to the usual HTML rules. Any number of space, tab, and newline characters are collapsed together into a single space. To generate a paragraph break, use the [$para] interpolation in a Text property. As a special case, a blank line (double line break) in Text is also treated as a paragraph break.

How text is refreshed

Normally, the text in your Seltani window is kept up to date automatically. Whenever any property of the Age changes, the text is updated accordingly. There’s a lot of mechanism behind this, but you don’t have to think about it when you’re designing your Age. It just works.

However, it may be useful to understand the principles.

When a piece of text is displayed, the server keeps track of every property that influenced the output. As long as your client remains connected, this list of properties is associated with you. Whenever a property value changes, the server checks to see which clients are displaying text that depends on that property. For each such client, the server re-renders the block of text and passes it along.

The Seltani game window displays four of these blocks of text:

  • The location description
  • The list of players in the location
  • The focus description
  • The instancepane description (in the right column)

Note that the location and the player list appear consecutive to the player, even though they’re separate blocks.

Each of the four blocks has its own dependency list. You can try the debug command “/dependencies” to see your client’s current lists, although the output is rather opaque. (You’ll notice a lot of repetition, for reasons which are not worth explaining here.)

As we said, you normally don’t have to worry about any of this. The displayed text is just always correct. However, there are situations worth noting.

Whenever a property is assigned to, the entire text block is re-rendered. This is most obvious in the focus description, which does a “new pop-up” animation even if only a small part of the text has changed.

Whenever a property is assigned to, the block is re-rendered even if the property value didn’t change. That is, assigning x = True when x was already True causes the text to be recomputed and sent to any clients who are watching. Because of this, it’s slightly more efficient to write:

   if not x:
       x = True

The current time (datetime.now) is not treated as a dependency. A block of text displaying this value will show the time of its last update, but it will not be refreshed when the time changes. (The time is always changing! If we treated this as a dependency, the displayed value would continually spin, which would be an enormous waste of server time.)

A more subtle error is to embed datetime.now in a conditional: [$if datetime.now < futuretime]...[$end]. Again, this will not be refreshed when the time changes. You should use a timer event which assigns a property.

The functions in random are also not dependencies. They will produce a different value every time they are called, but they will never themselves trigger a text block update.

Built-in functions do not always trigger dependencies when they should. For example, the pronoun functions do not trigger a refresh if the player changes pronouns. These are unlikely to be an issue, but please file a bug report if you find that you need one which is missing.

If the display code runs into an error and stops, later properties are not added to the dependency list. This may lead to surprising results if you’re working on an Age, make a mistake, and then try to fix it. If the display seems to be stuck, reload the browser window or type “/refresh”.

GenText: procedural text generation

A GenText property contains code that generates text. However, the code language is rather different from the code you’ve seen so far.

To demonstrate this, open the world editor and create a GenText property that looks like this (with the quote):

 "You see a thing."

Use this as a link, and you’ll see the focus window pop up, just as if you’d created a Text property. (Except for the quotes.) So why is this interesting?

Here’s a convenient way to chain together several values:

 Seq("You see ", 1, " thing.")

Okay, that’s still not very interesting – the Seq operator strings several bits of text together. You could do the same with a Text property:

You see [[ 1 ]] thing.

But here’s a new trick:

 Seq("You see a ", Alt("blue", "green", "red"), " thing.")

The Alt operator selects one of its arguments at random – or rather, not at random. The choice is always consistent based on the structure of GenText property and the identity of the instance you’re in. If you release an Age with this property in it, players in a given instance will always see the same value. (Technically this is a deterministic pseudo-random generator with the instance ObjectId as the seed.)

The syntax is simply a Python expression, made of nested function calls. That can be a bit verbose, however. You can simplify it a little. Square brackets can be used for the Seq operator, and parentheses for the Alt operator. So the above can be rewritten:

 ["You see a ", ("blue", "green", "red"), " thing."]

The value of this becomes more apparent when you begin building GenTexts on top of each other. For example, you could define one named color:

 ("blue", "green", "red")

And then make use of it:

 ["You see a ", color, " thing."]

By piling these together, one can construct a complex, variable piece of text which will always be consistent within a given instance. This is a powerful tool for customizing Ages.

There are several other GenText operators. The most interesting is Shuffle:

 Shuffle("blue", "green", "red")

This does a (pseudo)random selection like Alt, but the choice will never repeat until all choices are run through, and it will never repeat twice in a row. If you define the color property this way, you could make use of it like this:

 ["You see a ", color, " and ", color, " thing."]

This might produce “You see a red and blue thing,” but never “You see a blue and blue thing.”

Cooked text output

The examples above have been careful to add extra spaces around words, so that sequences don’t run together. As you write larger GenText structures, spaces become more of a nuisance. You need to worry about proper capitalization and punctuation as well; a phrase might occur at the beginning or end of a sentence.

The printing system can carry this load for you, although you need to explicitly turn it on. In this mode, spaces are automatically inserted between strings; capital letters appear whenever you apply a period. We call this “cooked mode” – in contrast to the default “raw mode”, which leaves spacing and capitalization alone.

Create a room in the build interface and set its desc property to:


Then create a Code property named display_scenery:

 gentext.display(scenery, cooked=True)

Finally, create a GenText property named scenery:

 ["you see", "a thing"]

This is all lower-case with no extra spaces or punctuation. However, it appears in the room description as “You see a thing.” That’s cooked mode.

Here’s a more ornate example:

 ["you see", A, color, "thing", STOP, color, "confetti rains down"]

The A token generates “a” or “an” (so you’d see “a red thing” or “an orange thing”, as appropriate). STOP is a period, followed by capitalization. So the result could be “You see a green thing. Red confetti rains down.”

Other break tokens include COMMA, SEMI (semicolon), and PARA (paragraph break). Redundant break tokens are merged, so you can use COMMA at the end of one phrase and at the beginning of another without worrying about double punctuation. Similarly, if STOP is followed by COMMA (or vice versa), the comma is dropped.

Cooked mode does not change the spacing and capitalization behavior inside Text properties – it assumes that you’ve already formatted the Text the way you want it. It also does not apply between the terms of a compound print(x, y, z) call. However, it does apply to the beginning and end of a print call.

Custom seeds

We’ve said that the random selection of Alt or () is based on the instance ID. In some cases, we might want it to be based on some other variable. (For example, the scenery in the Age of Fleuven is based on a counter that increases over time.)

To customize this, use the gentext.display function again:

 gentext.display(scenery, cooked=True, seed=5)

You can set the seed to any numeric, string, or ObjectId value. If you use a constant, as in this example, the random choices will be fixed everywhere, for every instance. If you use an instance variable, you can cause the choices to change by changing the variable value.

A warning: the pseudo-random generator combines the seed, the GenText property name, and the property structure to make its choices. This means that if you add new elements to a GenText, the entire output may change! There is no guaranteed way to add a new chunk while keeping the rest of the generated text the same.

This would not matter for Fleuven, where the scenery changes periodically anyhow; players are unlikely to notice. However, if you design an Age with a GenText sculpture in it, players might be annoyed if their personal instance sculpture changes. In these cases, it is best to leave the GenText structure unchanged after you release the Age.