Your First Game

Andy Brennan edited this page Feb 22, 2013 · 2 revisions

Alright! I'm happy you decided to try Satay! I think you'll like how easy it is to make simple games.

Now, I'm expecting you to already have Satay downloaded, and that you have a Python distro (Version 2.7) installed.

There's a text-based game example called agame.py if you want to see every feature of Satay in use. However, let's start small and we'll work our way up to the good stuff!

Imports

So, to start, make a fresh python file (call it taco.py if you're at a loss for names). When we want to make a new Satay game, first we'll have to import all our Satay classes and tools.

So, let's start with this:

from Satay.PorkStyleTextGame.Game import Map, Item, NPC, PorkStyleTextGame as GameMode
from Satay.Base import NumeratedList, Dynamic, DialogMap, Dialog, Response, Action, Condition, Event
from Commands.kill import kill, murder
from Commands.basic import look, go, get, take, drop, inventory, inv, i, save, load, quit
from Commands.talk import talk
from Commands.eat import eat

We've included the following:

  1. The Gamemode we wish to use.
  2. The basic Satay classes.
  3. Commands we wish to use.

So far, Satay only supports text-based games (though I am trying to get more mediums up and running). The Satay.Base and GameMode imports will remain much the same oftentimes. In this example, I chose the PorkStyleTextGame (named after an older project) as our Gamemode. We've also added several commands that come with the current revision of Satay.

Objects

Now that we've imported all our stuff, we can start working with it! to begin, make a simple dictionary and assign it to objects:

objects = {
    
}

This dictionary represents all the entities in our game. This means maps, items, and NPCs (or whatever entities the gamemode might've defined). This is where the meat of our game shall be. It's pretty simple to define an entity, too:

objects = {
    "mPuddle":Map(
        name="A Puddle",
        desc="You see a nice puddle here.",
        nbase="puddle",
        descriptors=["wet","nice","pleasant"],
        itemlist=NumeratedList(
            iTem=3,
            iStone=2,
            nMan=1,
        ),
        s="mHeree",
    ), # Leading commas are okay in Python! Remember that...
}

Wow, that's a lot of stuff, isn't it? Well, have no fear—I shall explain it!

First, we defined a Map object paired to the key mPuddle. Next, we defined its attributes (also called properties). Attributes can be of any type in Satay, and can be of any name. This means that you can make all kinds of useless attributes no command will ever use. Neat! Each attribute name is assigned a value (such as "A Puddle" for the name, etc.). Some attributes (e.g. name, desc, nbase, and descriptors) are required. Satay will raise an exception if you decide not to include these attributes in your definition. What's required of course depends on the entity being defined, and the gamemode it's being defined in.

name and desc are pretty straightforward here: a name and description of the entity. However, nbase and descriptors might not be. These two attributes are exclusive to text-based games. nbase refers to what the user must call the object in order for the game to understand the object they are talking about. Descriptors are adjectives the user may use to differentiate a particular object with the same nbase as another object. This means that if we want to talk about this puddle, we can refer to it as puddle or any combination of the descriptors (e.g. nice wet puddle).

Now onto the itemlist. This is an optional attribute for Maps. It defines what entities are inside the map, plus how many. A NumeratedList is always used for such lists in Satay, as it provides for easy definition of how many of something are in itself. So, in mPuddle, there are 3 items, 2 stones, and 1 man.

The final attribute used here is a directional attribute used by Commands.basic.go. This defines what is south of this map. In this case, mHeree.

All of these attributes will prove very useful in making a functional game.

More Objects and Dynamic Properties

This is pretty simple, isn't it? Now let's add some more items (plus that other map) to our game.

objects = {
    "mPuddle":Map(
        name="A Puddle",
        desc="You see a nice puddle here.",
        nbase="puddle",
        descriptors=["wet","nice","pleasant"],
        itemlist=NumeratedList(
            iTem=3,
            iStone=2,
            nMan=1,
        ),
        s="mHeree",
    ), 

    "mHeree":Map(
        name="Heree",
        desc="At the Wall.",
        nbase="heree",
        descriptors=[],
        n="mPuddle",
        itemlist=NumeratedList(
            iStone=2,
            iSword=1,
        ),
    ),

    "iTem":Item(
        name="Item",
        desc="A wondrous item for you.",
        nbase="item",
        descriptors=[wondrous'],
        kill_msg=Dynamic(
            "My item! NOESSS!!!",
            iSword="You killed my item with a sword!?!?",
        ),
        kill_newitem=Dynamic(
            "iStone",
            iSword="iPebble"
        ),
    ),

    "iPebble":Item(
        name="Pebble",
        desc="A small gray pebble.",
        nbase="pebble",
        descriptors=["small", "gray"],
    ),

    "iStone":Item(
        name="Stone",
        desc="A boring gray stone.",
        nbase="stone",
        descriptors=['boring','gray'],
    ),

    "iSword":Item(
        name="Sword",
        desc="A sharp, shiny sword.",
        nbase="sword",
        descriptors=['sharp','shiny'],
    ),
}

Alright, all this looks familiar, doesn't it? All but one thing. Notice kill_msg and kill_newitem (attributes used by the kill command). They are assigned to Dynamic instances. So, then, what does Dynamic mean?

In Satay, the Dynamic class serves as a way to define dynamic attributes. These attributes can change their value for commands based on particular prerequisites. How these are used depends entirely upon how the command operates. In Commands.kill.kill's case, the dynamic piece is the object with which the item is killed with.

kill_msg=Dynamic(
    "My item! NOESSS!!!",
    iSword="You killed my item with a sword!?!?",
),

The first argument to a Dynamic call is the default value for the attribute. Every argument after that is assigned to an entity identifier. In this case, if iTem is killed with iSword, Satay yields a different message and different new item. As you can see, this feature is very handy and powerful!

Events, Conditions, and Actions

Well, aren't you just a Satay wizard? Ha, no. That would be me. But you're getting there! Keep on reading, as I think you'll like the next few features.

Now, Events, Conditions, and Actions all go hand in hand, as Events use the other two. Conditions are used in Satay to define, well, conditions in which Actions shall be executed.

Conditions

Let's look at some example conditions:

Condition("some_list").Contains("some_element")

Condition("random_property") == 34 # Or any other boolean operator

Condition.History.Happened("verb").To("entity") # Can also use the .With("entity") function

The Condition class can either take a game variable (we'll talk about those later) or other exposed attribute of the Game (NOT an entity, the actual game itself). This allows us to query current game variables, check for items in the inventory, or other related things.

The last Condition is a little different. It is using the History component of Condition. With History, we can make conditions based on things the user successfully did (from checking the inventory to killing something with a certain object). You'll probably end up using this component the most. That, and checking for items in the inventory.

Actions

As I said, Conditions are used in conjunction with Actions. If a Condition proves true, often a list of Actions shall be executed. Here's an example:

Action("AddToInventory")("item")
Action("Replace")("one_item", "another_item")

First we call Action and pass in the function we wish to execute. Then, we create a separate tuple of arguments next to it (pretty spiffy, huh?). There are several functions that can be executed, and this list depends upon what functions that gamemode has created for you. See, Actions are pretty simple!

Events

Now to tie this all together with an Event!

Event(
    Condition.History.Happened("eat").With("iFork"),
    Action("Replace")("iFork","iChickenFork")
)

Event(
    Condition.History.Happened("eat").With("iFork"),
    [
        Action("Replace")("iFork","iChickenFork"),
        Action("Print")("Hai thar!"),
    ]
)

Events take one Condition, and then one or more (if more, a list) Actions to be executed upon that Condition evaluating as true. But where would we use events? Take a look as I add more objects:

"iFork":Item(
    name="Fork",
    desc="A shiny, silver fork. It's quite pointy!",
    nbase="fork",
    descriptors=['shiny', 'silver', 'pointy'],
    events=[
        Event( # :D Here's our event!
            Condition.History.Happened("eat").With("iFork"),
            Action("Replace")("iFork","iChickenFork")
        ),
    ],
),

"iChickenFork":Item(
    name="Chicken Stuck to Fork",
    desc="Some awful chicken glued to a now dirty fork.",
    nbase="fork",
    descriptors=['dirty', 'pointy', 'awful'],
),

"iEmerald":Item(
    name="Emerald",
    desc="A green and valuable emerald.",
    nbase="emerald",
    descriptors=['green', 'valuable'],
),

"iChicken":Item(
    name="Chicken",
    desc="Some juicy, cooked chicken.",
    nbase="chicken",
    descriptors=['juicy', 'cooked'],
    eat_edible=True,
    eat_message=Dynamic(
        "Gah! This chicken is awful.",
        iFork="Ewww. It seems glued to the fork now!",
    ),
),

Entities can also have an events attribute where we can define a list of Events to look for every time that entity is used in a command. In this case, whenever the fork is used to eat that seemingly delightful chicken, it will become glued to the fork, yielding a new item! Events are useful for defining exceptional post-command actions to take that aren't specified in a command. Such as this case here, the eat command doesn't constantly check for times items may become glued together (that's just silly!); instead, iFork independently checks for this occurrence.

NPCs and Dialog Trees

Now it's time to use all those skills you've been learning for the most complex (in this case that's a good thing!) feature of Satay: NPC dialog trees!

An NPC is defined like any other entity, only they have another required attribute: a dialog tree.

"nMan":NPC(
    name="A Man",
    desc="An old, aging man.",
    nbase="man",
    descriptors=['old','aging'],
    dialog=DialogMap(
        start=Dialog(
            "Hey there.",
            Response(
                "Hey.",
                'a0',
                Condition.History.Happened("talk").To("nMan"),
            ),
            Response("Sup.",'a0'),
            Response(
                "I killed the item with my sword.",
                'e2',
                Condition.History.Happened("kill").To("iTem"),
                Condition.History.Happened("kill").With("iSword")
            ),
        ),
        a0=Dialog(
            "Want some chicken?",
            Response("I like chicken.","a1"),
            Response("I hate chicken.","a2"),
            Response(
                "That chicken sucked last time.",
                "a2",
                Condition.History.Happened("talk").To("nMan"),
                Condition.History.Happened("eat").To("iChicken"),
            ),
            Response(
                "I already have some.",
                "a2",
                Condition("inventory").Contains("iChicken"),
            ),
        ),
        a1=Dialog(
            "Nice! So do I. Have some!",
            Response("Bye", "e1"),
            action=[
                Action("AddToInventory")("iChicken"),
            ],
        ),
        a2=Dialog(
            "Aw, dang.",
            Response("Bye", "e1"),
        ),
        e2=Dialog(
            "Excellent! Take this emerald...",
            action=Action("AddToInventory")("iEmerald"),
            end=True,
        ),
        e1=Dialog(
            "Good bye, then.",
            end=True,
        ),
    )
)

DialogMaps

A DialogMap represents a tree of dialogs for a particular NPC. A start Dialog must be defined as an entry point, and then all other Dialogs may be given arbitrary names.

Dialogs

A Dialog takes 2 arguments minimum: the first being the NPCs dialog, the next few being one or more Responses for the player. Either one or a list of actions may be also defined for a Response as seen in the e2 and a1 dialogs in the example. Also, if the Dialog is an exit point, end is assigned to true as seen in e1 and e2. Remember to make exit points for your dialogs if you want your user to be able to stop talking to the NPC!

Responses

Responses are the last component of NPC dialog trees. These are things the player may say, and Conditions may be set that way only certain Responses appear at certain times. Each Response first takes the player dialog, the new Dialog to redirect to if this Response is picked by the player, and then an unlimited amount of Conditions in which the Response may appear. As you can see in the example, this is the same Condition as I explained earlier!

Setting the Settings

Alright! Now you know all the basics of Satay TextGames and derivatives! Of course, we haven't told Satay to do anything with all these definitions. What next? Take a look:

settings = {
    "start":"mPuddle",
    "title":"A Game",
    "author":"Andy Brennan",
    "items":NumeratedList(
        iTem=3,
        iSword=1,
        iFork=1,
    ),
    "objects":objects,
    "commands":[kill, murder, talk, look, go, get, take, drop, inventory, inv, i, save, load, quit, eat],
}

In this dictionary, we set several basic settings for our game, including the Map to begin on, a title, an author, starting inventory items, the object dictionary we have been creating, plus a list of commands we wish to use. Game variables are also created here with defaults in a variables dictionary. After we've created the settings dictionary there's but one final piece to make...

Run the Game

Now for the part that makes all the magic happen:

if __name__ == "__main__":
    aGame = GameMode(settings)
    aGame.Run()

The if statement at the top is a Pythonic "best-practice" that way this file only executes as a game if it is the main module. The next line creates the game from the GameMode class we imported with the settings we just built as its only argument. Once we've created the game, we just call the .Run() function on the game, and Satay takes it from there!

Distribution

If you want to distribute your game as a Windows executable (Linux/Mac execs currently not supported), simply run Satay.Utilities.CreateWinEXE in your Python interpreter. It will give you explicit instructions on what to do, and within a few seconds you'll have yourself a ready-to-distribute game! Also note that CreateWinEXE only works in Windows.

Wrapping Up

Well, we've created an entire Satay game from scratch. Nice job! The only limit to Satay is your imagination, so make something with it! Of course, if the occasion does arise where you find something impossible to do in Satay, make a GitHub issue and I'll see what I can do.

Remember that if you want Satay to really work for you, you'll need to learn Python so you can make your own Commands (or even gamemodes).

So, in closing, have fun and create something new!

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.