Perl Edition
Note / Lame Excuse
As of August 2005, the information is this document is rather out of date; the Volity protocol has developed significantly since this page was first written, and the Perl development tools have evolved too. We hope to have a more accurate version of this documentation up in the near future. Sorry for the inconvenience.Abstract
This article details the process of using the Perl programming language to create modules for Volity, a platform for Internet-based game playing.
Table of Contents
This article details the creation of game modules for Volity, a platform for turn-based, networked computer games. While many concepts covered herein are universally applicable to all Volity game programmers, all the examples and language-specific discussion assume that the reader is working with the Perl programming language.
First, if you haven't already, please read the Volity design overview article. It defines the platform's key concepts, and while you don't need deep understanding of how Volity works in order to write games for it, you should have at least a surface familiarity of its important elements and lingo.
That done, get yourself a copy of Frivolity, a collection of Perl libraries and programs for creating Volity modules and then running then as servers on the Volity network. The most recent version is always available as an easy-to-install tarball from the Volity.org website. Even though you'll be working principally with only the Volity::Player and Volity::Game packages, they depend on other modules within the Volity namespace, so you'll want to install the whole shebang.
Once you are thus prepared with both knowledge and power, you are ready to start working on your game. Sort of. Before you can actually begin programming, you see, you must plan your approach by deciding which Volity ruleset your module shall adhere to — and there's a strong likelihood (especially in these, the early days of the Volity empire) that you'll end up defining this ruleset yourself. Fortunately, if you think that programming game logic is fun, you'll probably get a kick from ruleset definition as well.
In Volity terms, a ruleset expresses a game's concept by defining the communication that can take place between the referee[1] and the players while that game is in progress. Effectively, it's an API for that type of game. Chess, for example, has a rather simple ruleset: players can ask to move a piece from one square to another, and referees can inform all players that a piece has moved, or decline an individual player's request to move a piece (either because the move isn't legal, or the piece didn't belong to the player, or it's not currently the player's turn). We'll cover some other examples later in this section.
Every ruleset possesses, as its label, a globally unique URI, and ruleset implementations — such as the module you're chomping at the bit to create — assert which ruleset they adhere to by posting this URI in some conspicuous location. Were you making your own version of a Volity Chess game, you'd want to have your module "know" that it declares fealty to http://volity.org/games/chess (which happens to be the Volity's URI for basic chess). Your module, when run as a game server, would use this URI whenever it needs to identify itself to the system at large, so that the Volity bookkeeper[2] knows how to categorize your server and the game records that it produces. If, on the other hand, you were making your own game (perhaps a chess variant of your own invention), then you'd have to go through the process of creating your own ruleset definition, topping it with a URI of your own, and then linking your module to that.
So how do you know whether or not a given ruleset already exists? Currently, the best way involves visiting the Volity.net website and browsing the rulesets described there. (If you're in doubt about whether or not an existing ruleset is the same as that which you have in mind, just ask me about it.)
If someone else has already defined the ruleset you wish to implement, congratulations; your task may have become a bit simpler. You will want to take note of the ruleset's URI, which your code will need to know about, as well as the API that the ruleset defines; this latter information
Note
At this time, Volity has no plans to police the namespace of rulesets, leaving open the possibility of "pollution" by people making bogus rulesets. Since anyone can make a ruleset called "Chess", someone can easily create a ruleset document that poorly or incorrectly defines that game, leading to confusion when someone wishes to implement it (or, worse, when a player is looking for available game server categories) since there might be multiple URIs claiming to be the unique label for chess.
The hope is that the network will be self-correcting, through the fact that the bookkeeper has knowledge of who implements what. As game servers register themselves, and are then played and enjoyed by real players, then the "real" rulesets will stand out, leaving the more bogus ones (which no sane, enjoyable game server uses) in the dark. However, time will tell how well this will work in practice. For the time being? Don't worry too much about it.
Volity is young, and the world is a very big place. Like as not, your game ideas are virgin turf for Volity, and you'll need to define your own ruleset. (And if you're approaching this platform with your own game design ideas in mind, this was rather a predetermined outcome anyway) Congratulations, you're in for some fun. (If you're into this sort of thing. And I just know that you are.)
Creating a ruleset involves thinking about the programmatic communication that must occur between referee and players while a game is taking place, and then creating a pair of appropriate APIs, one for each direction of this conversation. You then top this mixture with a URI of your own making, and let the bookkeeper know of your new concoction.
For your first trick, you will create a list of the requests that a player of the game can send to the referee. This list must be abstract enough that it remains independent from any user interface which might be applied to the game. In other words, it should not concern itself with things as low-level as player-side mouse movements or keystrokes, or even button-clicks, menu selections, or any other action that the human player might perform; instead, it defines only the final messages sent to the referee as a result of all that furious typing and clicking.
These requests should contain no actual game logic. They are merely the communication vehicle between player and referee; it's up to the ref to decide is the messages it receives actually make any sense. Take the following line from Volity's chess ruleset:
move_piece(start_square, destination_square)
Since it's defined in the player-to-referee API, a player can send this request to the referee at any time during the game, even if (by virtue of its arguments, or even of simple timing) the player is asking for the impossible. So, the referee will happily receive requests to move a pawn ahead by three spaces, or start a move from an empty square, or even move a piece when it's not that player's turn.
Tip
You can think of all player-to-referee requests are just that — requests that the referee do something — and not commands, per se.
There are, informally, two types of player-to-referee requests. Beyond the conceptual difference, they also differ in the kinds of return values they expect from the referee.
- Action requests
Requests that the referee make some change to the game state, on the player's behalf. This could involve moving pieces, playing cards, rolling dice, and so on.
If the referee can't carry out the request due to a conflict with the rules, then it sends back a fault containing one of the ruleset's error codes (see the section called “Defining error codes”). Otherwise, the request succeeded, and the referee sends back a generic success response.
Since that player's action will almost always change the game state in some way visible to at least one player, the referee will usually follow up a successful request with a few requests of its own, as appropriate. For example, after accepting a player's request to move a piece on the board, the referee would then send all the players a message (defined in the ruleset's Referee-to-Player API section) informing them that the player has done this.
- Information Requests
Information requests are simply methods to ask the referee about the game state. The referee provides its answer as its response value.
These requests tend to be rather uncommon, since most rulesets' APIs are set up so that the referee continually pushes game-state information at the players. However, these sorts of requests can still prove handy in certain hidden-information games.
Volity's model game, Rock-Paper-Scissors ("RPS" for short)[3] has a very simple player-to-referee API, since a player can perform only one type of action. And here it is, with attendant prose description:
choose_hand(hand_type)
The player declares that he "throws" the given hand. The argument must be one of "rock", "paper", or "scissors".
This means that a Volity game module that claims to support the URI http://volity.org/games/rps will implement some way of receiving the choose_hand() player request, and reacting to it appropriately, probably by interpreting the argument, and then either returning a fault (if it didn't recognize the argument as a hand type) or modifying its internal representation of that player (if it did).
Since this is the only request defined in the RPS ruleset's player-to-referee API, any other call sent to the referee will result in a fault sent back to the player. (As a game programmer; you needn't worry about this situation; the base referee module will automatically return faults for unrecognized player requests.)
For a bigger example, let's have a look at Volity's Crazy Eights player-to-referee API, as defined by http://volity.org/games/eights/index.html.
- play_card(card)
The player wishes to play the given card, which must either match the last card in suit or rank, or match the declared suit (if one is active), or be an 8.
If this play succeeds, and the current player is left with no cards, the game immediately ends.
Possible error codes: 901, 902, 903
- choose_suit(suit)
After playing an 8, a player must (unless it was his last card) follow up by using this function to declare a suit. The turn does not advance until this happens.
The suit argument should be a suit's one-letter initial (C, S, H or D), but a polite referee implementation can be more liberal in what it accepts. If it can't figure out what suit is intended from the argument, it should return an error 906.
Possible error codes: 901, 905, 906
- draw_card()
The player wishes to draw the top card from the stock, and add it to his hand.
Possible error codes: 901, 904
We can see from this excerpt that a proper Crazy Eights implementation must handle these three (and only these three) player requests. As with our RPS example, they all result in either the ref sending a fault to the player, or changing its internal representation of the game state in some appropriate fashion (and then telling the players what just happened, which we'll cover in the section called “Referee-to-Player API”). (And in the section called “Creating a Frivolity Game Module”, we'll talk about what these mysterious game-state changes are all about, in terms of actual code.)
Note how each request's description concludes with a list of possible error codes. These mysterious codes are spelled out elsewhere in the same document, and handle outlier situations such as the player acting out of turn (which happens to be the ubiquitous 901), or attempting to play a card he isn't actually holding. We'll talk more about how they work in the section called “Defining error codes”.
Since the referee must tell the players — quite often all at once — about every player-knowable change in the game state, the referee-to-player API tends to run much longer than its complement, with functions for every sort of information that a game's referee might need to pass along to players. The attitude of these messages also differs from the player-to-referee methods, more resembling assertions and commands than requests. While running a game, the referee is the lone arbiter of that game's reality, and the players take its word as the unquestionable truth.
As with the player-to-referee API, one can informally divide methods defined by a ruleset's referee-to-player API into two categories. The division comes, however, by the number and nature of intended receivers. There is no real difference in the way the referee sends them, or in how client applications receive and react to them.
- Calls to all players
These messages are broadcast to every player at the table, in order to inform everyone that the game state has changed in some publicly knowable way. A common first argument to these calls is a player's name, since a referee often blasts out these table-broadcasts in order to inform the players that one of their number just did something interesting, such as moving a piece or rolling a die.
- Calls to specific players
Some messages are eyes-only, either due to in-game hidden information (as with a player drawing a card into his hand) or from a referee otherwise needing to relay something to a single player (or a group of players smaller than the entire table.
Let's see what our old friend RPS has as its referee-to-player API. Actually, this simple game contains only a single function in that direction:
- player_chose_hand(player, hand)
The named player has thrown the given hand. Sent after the referee has collected hand information from both players.
And that's that, for this trivial game. Note that the ruleset assumes that the client-side UI file is smart enough to declare a winner for itself, based on the results of the two player_chose_hand calls that it will receive. It didn't have to do it this way; we wouldn't necessarily have slapped the Considered Harmful label onto a declare_winner(player) function, or the like. It just happens that this particular ruleset author[4] prefers to make communication protocols as small as possible[5].
Crazy Eights, being a more sophisticated game than RPS, has an API to match.
- player_played_card(player, card)
The named player played the given card, which is now the card to match.
- player_chose_suit(player, suit)
Having played an 8, the named player has declared the given suit to be the one to follow.
- player_drew_card(player)
The named player drew a card from the stock.
- player_passed(player)
The named player, unable to either play or draw, had to pass.
- scores({player=>score, player=>score, ... })
Called when the game ends. The given struct is a hashtable of all the game's players, and their score for this game, representing the value of the cards they were left holding at the end.
Sorting the players in ascending order by their respective scores will reveal the ranking for this game. Exactly one player will have a score of 0, indicating that they held no cards at the end; that player has won this game.
- draw_card(card)
The receiver of this call has drawn the given card.
Here, the latter two functions are meant to the directed at individual players, as they deal with information that only those players should see (that being the rank and suit of the cards). Note that, when a player successfully draws a card (by sending the draw_card() request seen in the section called “The Player-to-Referee API”), the referee reacts by sending two messages to the players: the secret draw_card() to the player who actually made the action, and the public player_drew_card to the entire table, thus informing everyone about that player's increased hand size, without spilling the beans as to the card's specifics.
The referee-to-player function-call API really defines only half of the communication that the ref can send players over the course of a game. Error codes provide the rest, giving the referee a way to bark at players about what they're doing wrong, using predefined code numbers so that players' UI files can (if they choose) interpret these errors in creative ways.
The error-codes section of a ruleset definition document lists all the possible fault-responses that the ref might choose to blast back at players after receiving faulty requests from them. By convention, these codes exist in the 900-range, and each is paired with a suggested error message that should accompany the code.
Let's have a look at the RPS and Crazy eights error-code tables.
Table 2. Crazy Eights error codes
| Code | Message |
|---|---|
| 901 | It's not your turn. |
| 902 | You don't have that card. |
| 903 | Unknown card. (Malformed play_card() argument.) |
| 904 | You can't draw a card; the stock is empty. |
| 905 | You can't choose a suit; you haven't just played an 8. |
| 906 | Unknown suit. (Malformed choose_suit() argument.) |
Note that codes have a particular meaning only for a single ruleset, as 901 is defined two different ways in the above two examples.
With all the APIs and error codes for your ruleset defined, you're nearly done! The final step involves choosing a URI to use as the ruleset's label, and then registering it with the Volity bookkeeper.
Volity places no restrictions on what you may use as a URI. While rulesets written or maintained by members of the core Volity development team often have URIs falling within the volity.org domain, ruleset authors are encouraged to use any URI under their own control. Optimally, a ruleset URI will also act as a URL, pointing at a Web page that, when visited with a browser, results in a page summarizing the ruleset (perhaps with additional, human-readable game rules and other notes), making it an excellent resource for developers and players alike.
When you have chosen a URI, take it over to http://volity.net and use the tools there to make it official. Congratulations! You can now move on to the really fun part — making your game module actually happen, by building a Perl module for it.
Note
This is when we start getting very Perl-specific. To make the most sense from the remainder of this article, you should grok object-oriented Perl.
Frivolity, Perl's Volity implementation, makes the creation of Volity modules in Perl very easy. It involves the creation of an object class that subclasses the stock Volity::Game module, which (as Figure 1 shows) plugs into the black box of the Frivolity game server application. Whenever a game; the module becomes, in effect, a driver for the referee object.
You could think of the Frivolity server as a video game console, and the Perl-based game modules it can run as interchangeable cartridges; only one can be plugged in at a time, but while it is, it completely defines how the game console presents itself to the world, without having to perform any changes to the console itself.
Depending upon the needs of your game, you might also want to subclass the stock Volity::Player module. You can almost always leave untouched the various other modules that go into a running Frivolity game server, such as Volity::Server and Volity::Referee, as they're flexible enough to handle a wide variety of games. Frivolity does provide methods for running the server with subclasses of even these core modules if you need to perform some deep voodoo, but this article leaves this as an (unlikely) exercise for the reader; see the Volity manpages for more information.
Before getting into the gory details of writing game objects, let's have a look at the life cycle of one of these interesting creatures.
In Frivolity, a table's referee begins its interaction with its game module before the game even begins. When all the players at the table signal their readiness to play, the ref calls some methods, such as min_allowed_players(), on the game class to make sure that the game can start under the present conditions.
Once the ref confirms that the game can begin, it finally creates the game object by calling the game class's new() constructor. It then stores this object in its own game field, and also puts a reference to itself inside the game object's referee field. (Creation of this field — and properly managing the circular reference that its presence causes — are both handled for you by the base class.)
After completing all the necessarily initialization on its own end, the referee calls the game's start_game() object method, and from that point on the game object has full control of things. Even though the referee remains the entity which at players aim their game-related communication, the ref simply passes all the game RPC requests players send to its underlying game object, who reacts to them however it deems best. Similarly, the referee holds the game's public face when sending messages out to players, so the game object calls methods on the referee when it wants to respond to players' RPC requests, or otherwise communicate with the folks at the table.
Note
To make things convenient for programmers, a Volity::Game subclass rarely if ever needs to directly refer to the referee. The base module's predefined communication methods handle most of the game-to-referee message-passing that needs to take place. If you ever do need the referee object for some nefarious purpose of your own devising, don't forget that it's always sitting right there in the game object's referee field.
Eventually, the game decides that it's over, most likely because a winning condition has been met. It sets a few important fields on itself, such as the final ranking of all the players in the winners field, and then calls its own end_game() method. Defined by its base class, this sets in motion several events that culminates in the referee sending a record of the completed game to the Volity bookkeeper.
This is the end of the object's life; the referee destroys it, and then waits at the table to see if the players wish to play again. If so, it instances the game class once more, and the game's circle of life begins anew.
Because Frivolity game modules must fulfill certain requirements in order to work, they tend to have a predictable structure, including the following features. We'll get into the details of each section in the section called “Writing the module”.
- Perl OO Declaration
The module must subclass, through the use base pragma, the Volity::Game Perl module. This lets your module inherit some crucial object and class methods, as well as object fields and class data.
- Ruleset configuration
By setting some class data, you let your module know which ruleset URI it adheres to. You also set some configuration information that your game needs in order to let the referee run a "pre-flight check" before creating the game object, such as the minimum and maximum number of players that the game supports.
- Referee callbacks
There are a handful of methods that the Frivolity referee might call on your game object after it's instanced. For example, it will call start_game() when it's all done setting up the playing table, in order to signal that it's ready for any initialization that the game might require (such as dealing out cards or setting up the board). The Volity::Game module provides some default methods for these, which you can override in your own module for custom behavior.
- Player callbacks
These important methods provide the players' interface to your game. Generally, you must create exactly one of these subroutines for every player-to-referee method defined in the ruleset's API (see the section called “The Player-to-Referee API”). Players will call them via RPC. (They can't accidentally (or "accidentally") call any other methods, due to Frivolity's simple RPC security mechanism, which we'll cover later.)
- Internal methods and functions
This is just the freewheeling part of your code, that which isn't a callback or a configuration setting, and instead houses all the game's internal logic. Generally, these functions are triggered by player callbacks, since most of a game object's activity comes about in reaction to player input.
- Player Classes
The Volity::Player class defines a fairly generic player-object, with a few basic fields and methods pre-defined. This is fine for many games, but many others will want to customize this class a bit; for example, giving them a hand field for holding cards. These game implementations are free to subclass Volity::Player, and include the package definition in the same file as the game module.[6] You then let the referee know what kind of players to hand the game object by setting a class variable.
This section covers the fields you must define, methods you must override, and other work you must do for your game module to come alive. You should, however, refer to the Volity::Player and Volity::Game manpages as final-word references on those base classes and their methods.
For the remainder of this section, we're going to build, piece by piece, a simple Frivolity module that plays Rock Paper Scissors, using the ruleset API that we sketched out in the section called “Creating your own ruleset”.
The first step, of course, involves creating a new Perl module that subclasses Volity::Game. See the section called “Completing the RPS example module” or the section called “A more complex example: Crazy Eights” if you need a nudge in this direction.
Frivolity uses Perl's fields pragma for defining inheritable object fields[7]. Three of these fields are of particular interest to game module programmers.
- players
Contains a list of player objects, belonging to the Volity::Player class, or whichever subclass you declare through the player_class configuration field (as discussed in the section called “Configuration”).
When the referee creates the game object, it places all the game's players into this field, in no particular order. After it awakens, your module can reorder them according to some scheme if it wishes.
- winners
When the game ends, your module puts a winner-ordered list of the players into this field, as described in the section called “Ending the Game”.
- referee
The referee object, which you can do whatever you'd like with (including completely ignoring it, as most game modules probably end up doing). It's a direct reference to the real, live referee, so feel free to call any of the methods described in the Volity::Referee manpage. You can even call methods that would be obviously detrimental to the health of your game (such as having the referee leave the table and disconnect from the Jabber network), but you probably wouldn't want to.
To configure your module, you call some methods that set some class data fields.[8]
- max_allowed_players
The maximum number of players that this game can support. If unset, the game allows any number of players to join.
- min_allowed_players
If set, asserts the minimum player-population of a table before a game can begin. Otherwise, the referee won't make this minimum-population check before a game starts. (Note this field should be set for any game that doesn't have a solitaire mode!)
- uri
The URI of the ruleset that this module implements (as discussed in the section called “Choosing a ruleset”).
Unlike the other configuration fields, this one is mandatory; you must define it in order for you module to work.
- player_class
If your module uses a custom player class (instead of the default Volity::Player class), set this field to the class's name. The referee will create player objects of that class before the game begins.
Tip
To define a game that supports exactly two players, set both min_allowed_players and max_allowed_players to 2.
With that in mind, let's start programming our RPS game. We'll write the necessary Perl-module handwaving, and then add in the configuration information.
package Volity::Game::RPS;
use warnings;
use strict;
# The following line makes this an actual Frivolity game module!
use base qw(Volity::Game);
__PACKAGE__->max_allowed_players(2);
__PACKAGE__->min_allowed_players(2);
__PACKAGE__->uri("http://volity.org/games/rps");
1;
Actually, an RPS player should probably have a field to store the hand-shape that they've chosen to throw. So, let's declare and then create an appropriate subclass of Volity::Player which defines a hand field. After adding in the appropriate lines, our module file now looks like this:
package Volity::Game::RPS;
use warnings;
use strict;
# The following line makes this an actual Frivolity game module!
use base qw(Volity::Game);
__PACKAGE__->max_allowed_players(2);
__PACKAGE__->min_allowed_players(2);
__PACKAGE__->uri("http://volity.org/games/rps");
__PACKAGE__->player_class("Volity::Player::RPS");
# The remainder of this file defines our player subclass. We'll insert
# edits to the game subclass above this line.
package Volity::Player::RPS;
use base qw(Volity::Player);
use fields qw(hand);
# That's it. We just wanted to add the 'hand' to the class's
# object-field list, which will automagically create a hand()
# accessor method for us.
1;
We don't need to make any further changes to that player class, so all the example code for the remainder of this section will go above its package declaration. (We'll have a final look at the entire file, with both packages, after we're done putting the all the pieces together.)
Your module can optionally define any of the following object methods. They all exist as no-op stub methods in the base class, so you can choose to not override them. However, it's likely that you'll want to override at least start_game in order to define custom setup activity.
The referee calls each of these methods at certain points during the game object's lifetime.
- start_game
The referee calls this method after it has finished all of its preparations for a new game. This is the perfect opportunity for your module to perform any initialization is has to, just as setting up the game board, or dealing out cards to players. All the objects described in the section called “Preparation” are guaranteed to be defined at this time, to do with what you will.
In our RPS module, we override start_game to set all the players' hands to undef. Even though the player objects' hand fields are already undefined when they are first created, they will have a value (whichever hand type the player last threw) if the players played in the previous game at this table.
sub start_game {
my $self = shift;
for my $player ($self->players) {
$player->hand(undef);
}
}
Your module must define one method for every function defined by the ruleset's player-to-referee API. Furthermore, each method must be named after the API function it implements, prefixed with "rpc_" (which offers a bit of network security, since the referee prepends that string to all the RPC requests it receives for the game). This is true even if it breaks Perl subroutine-naming convention, as when a ruleset's API functions use StudlyCaps, for example. Since players invoke these methods directly upon your object via RPC, you've got no say in that matter (unless, of course, you also happen to be writing the ruleset API).
Each of these player-called methods should do something appropriate for the game, typically some combination of making internal state changes and calling UI functions on players (see the section called “Talking to Players”).
Mind the return values of these callback methods! They have a direct effect on the RPC response message that the referee will send back to the player, as follows:
A false return value (or an undefined one) means that everything went OK and the game module has no particular comment about it. The player who made the original RPC request will receive a generic success response from the referee.
This is the proper response for most state-affecting player actions, and as such you'll commonly finish up state-changing activity with a plain old return; statement.
A true return value that is anything except for a list whose first value is the literal string "fault" will be passed back to the calling player as-is as the RPC response. It still means that the request executed successfully, but adds some additional information that the player might be interested in.
Use this style of return value when reacting to players' information-request calls.
Finally (and I bet you saw this coming), a response consisting of a list whose first value is the literal string "fault" means that the game cannot fulfill the player's request for some in-game reason. The second member of this list must be one of the error-code numbers defined by the ruleset's error section (see the section called “Defining error codes”). As an optional third argument, you can include another string with a prose error message; it's up to the receiving UI how (and if) to display this.
According to its ruleset API, Rock Paper Scissors has but one player-to-referee call, choose_hand. Let's add that to our code now, but really just have it call another method, since that's going to contain the entirety of our game's logic! We'll see how that works in the section called “Completing the RPS example module”.
sub rpc_choose_hand {
my $self = shift;
return $self->set_hand(@_);
}
The referee-to-player section of the ruleset's API should define everything you'd ever need to tell your game's players. You don't need to implement these functions yourself; that's the job of the client-side UI file. Your game module simply needs a way to call these functions, and Frivolity gives you two ways to do this.
The Volity::Player base class defines a call_ui_function() object method, which takes as arguments the name of a player-side function, and any of its arguments you might like to pass along. The referee then uses RPC to call this function on the player represented by the invocant object.
For example, if we were implementing Crazy Eights, then we would need to reaction to a player drawing a card by putting some card's name into a variable named $card, and then sending it to the player via the API-defined draw_card(card) function. So, if that player happened to be in $drawing_player, then we'd say:
$drawing_player->call_ui_function('draw_card', $card);
Quite often (perhaps more often than not), you'll want to communicate something game-related to everyone at the table. You could individually invoke call_player_ui() on every player stored in the game's players field, but you'd probably be happier using the convenient call_ui_function_on_everyone() object method that's attached to your game object. It takes the same arguments as the other method, and does just what its name implies, invoking the named function (with included arguments) on every player.
This is actually the salient method as far as our Rock Paper Scissors implementation is concerned, since the referee of this game never has to send private messages to either player. And as it happens, the only time that it must send messages at all happens when the game ends, and it reveals both players' hand-throws to everyone at the table.
Finally, you can use ordinary Jabber messaging to communicate, sending chat messages to the table or to individual players (or even other people and entities on the Jabber network, if you really want to). The game classes don't offer any native way to do this, but you can instead pull strings on the encapsulated referee object to make it talk; for example, to send a friendly greeting to everyone in the table, where $self is a game object:
$self->referee->groupchat("Hello, everyone!");
Consult the Volity::Jabber manpage to learn about groupchat and the other chatty messages. And note that this chatting absolutely should not stand in for the API-defined referee-to-player calls; it should only be used for "color", or perhaps clever, off-spec hacks of your own devising, if used at all.
When the game has ended, you must tally up the results, and then let the referee know that it's time to wrap up.
First, create a new list comprising the players in winning order. That is, the person who won the game has their player object as the first member of the list, the second-place player is the second member, and so on down through whomever came in last. In the case of a tie for any position, use a list reference for that position, containing all the player objects who share that spot.
Note
Not all games have a sense of ordered winning; lacking any sort of score or other gradient victory-metric, they instead end with one clear winner and several lowly losers. In this case, just lump everyone who didn't win into the second-place spot, sharing the same list reference.
Once you've made this list, assign it to the game object's winners field, and then call the game's end_game() method, with no arguments. The referee takes care of the rest, destroying the game object in the process.
Warning
You can, if you wish, override the end_game method within your subclass, if you wish to perform some last-minute cleanup before your object goes away. If you choose to do this, you must call $self->SUPER::end_game within your method definition, or the referee won't know that the game has ended!
Now that we know how to receive and react to player activity, we can complete the RPS module, with a single, rather lengthy method that handles the ruleset's single player-to-referee function. Here's what the entire file looks like, then:
package Volity::Game::RPS;
use warnings;
use strict;
use base qw(Volity::Game);
__PACKAGE__->max_allowed_players(2);
__PACKAGE__->min_allowed_players(2);
__PACKAGE__->uri("http://volity.org/games/rps");
__PACKAGE__->player_class("Volity::Player::RPS");
sub start_game {
my $self = shift;
for my $player ($self->players) {
$player->hand(undef);
}
}
sub set_hand {
my $self = shift;
my ($player, $hand) = @_;
if (substr("rock", 0, length($hand)) eq lc($hand)) {
$player->hand_type('rock');
} elsif (substr("paper", 0, length($hand)) eq lc($hand)) {
$player->hand_type('paper');
} elsif (substr("scissors", 0, length($hand)) eq lc($hand)) {
$player->hand_type('scissors');
} else {
# I don't know what this hand type is.
return (fault=>901);
}
# Has everyone registered a hand?
if (grep(defined($_), map($_->hand_type, $self->players)) == $self->players) {
# Yes! Time for BATTLE!
# Sort the players into winning order.
my @players = sort( {
my $handa = $a->hand_type; my $handb = $b->hand_type;
if ($handa eq $handb) {
return 0;
} elsif ($handa eq 'rock' and $handb eq 'scissors') {
return -1;
} elsif ($handa eq 'scissors' and $handb eq 'paper') {
return -1;
} elsif ($handa eq 'paper' and $handb eq 'rock') {
return -1;
} else {
return 1;
}
}
$self->players);
# Tell both players what their opponent chose
for my $player (@players) {
$self->call_ui_function_on_everyone('player_chose_hand',
$player->hand,
);
}
# Tell the players who won, using Jabber messaging (just because we can).
my $victory_message;
if ($players[0]->hand_type eq $players[-1]->hand_type) {
$victory_message = sprintf("A tie! Both players chose %s.", $players[0]->hand_type);
foreach (@players) { $_->call_ui_function(game_over=>'tie') }
$self->winners([[@players]]);
} else {
if ($players[0]->hand_type eq 'rock') {
$victory_message = sprintf("%s(rock) crushes %s(scissors)!", $players[0]->nick, $players[1]->nick);
$self->winners($players[0], $players[1]);
} elsif ($players[0]->hand_type eq 'scissors') {
$victory_message = sprintf("%s(scissors) shreds %s(paper)!", $players[0]->nick, $players[1]->nick);
$self->winners($players[0], $players[1]);
} else {
$victory_message = sprintf("%s(paper) smothers %s(rock)!", $players[0]->nick, $players[1]->nick);
$self->winners($players[0], $players[1]);
}
}
$self->referee->groupchat($victory_message);
$self->end_game;
}
# At any rate, return undef, so the calling player gets a generic
# success response message.
return;
}
####################
# Incoming RPC request handlers
###################
sub rpc_choose_hand {
my $self = shift;
$self->set_hand(@_);
}
package Volity::Player::RPS;
use base qw(Volity::Player);
use fields qw(hand_type);
1;
Now we'll cover a more complex game module, one that plays Crazy Eights, according to the ruleset APIs for that game which we defined earlier in this article. This task is rather more interesting than implementing RPS, since the player-to-referee API now contains several commands, and we will make use of a third-party Perl module — Games::Cards, by Amir Karger[9].
package Volity::Game::CrazyEights;
use warnings;
use strict;
use base qw(Volity::Game);
# Fields:
# deck: A Cards::Games::Deck object.
# discard_pile: A Cards::Games::Pile object.
# last_8_suit: The initial of the suit that the last 8-player called.
# last_card_player: The player who has most recently played a card.
use fields qw(orig_deck deck discard_pile last_8_suit last_card_player);
use Games::Cards;
################
# Configuration
################
# Configure some class data, using field names inherited from Volity::Game.
__PACKAGE__->max_allowed_players(5);
__PACKAGE__->min_allowed_players(1);
__PACKAGE__->uri("http://volity.org/games/eights/index.html");
__PACKAGE__->player_class("Volity::Player::CrazyEights");
################
# Callbacks
################
# The server will call start_game on us. Crack open a new deck,
# shuffle it, and kick the players.
sub start_game {
my $self = shift;
my $game = Games::Cards::Game->new(
{cards_in_suit=>{
Ace=>1,
2=>2,
3=>3,
4=>4,
5=>5,
6=>6,
7=>7,
8=>50,
9=>9,
10=>10,
Jack=>10,
Queen=>10,
King=>10,
}
});
# Set up all the different card-holding objects we'll need.
$self->orig_deck(Games::Cards::Deck->new($game, 'deck'))->shuffle;
$self->deck(Games::Cards::Stack->new($game, 'draw'));
$self->orig_deck->give_cards($self->deck, 'all');
$self->discard_pile(Games::Cards::Stack->new($game, 'discard'));
# Tell the players that a new game has begun.
foreach ($self->players) { $_->hand(Games::Cards::Hand->new($game, "$_")) }
$self->call_ui_function_on_everyone('start_game');
# Deal 'em out...
$self->deal_cards;
# Flip the starter.
$self->flip_starter;
# Announce the first player's turn.
$self->call_ui_function_on_everyone(start_turn=>$self->current_player->nick);
# Have the first player draw cards, maybe.
$self->make_player_draw;
# Now we sit back and let the first player make his or her move...
}
################
# Player Actions
################
sub rpc_play_card {
my $self = shift;
warn "In rpc_play_card.\n";
my ($player, $card_name) = @_;
unless ($player eq $self->current_player) {
return(fault=>901, "It's not your turn!");
}
my $hand = $player->hand;
my $card_index;
eval { $card_index = $hand->index($card_name); };
unless (defined($card_index)) {
return(fault=>902, "You don't have that card ($card_name).");
}
my $card = $hand->cards->[$card_index];
# We have a card... is it a legal play?
if (
($card->name eq '8') or
($card->name eq $self->last_card->name) or
(
($self->last_card->name ne '8') and
($card->suit eq $self->last_card->suit)
)
or
(
($self->last_card->name eq '8') and
($card->suit eq $self->last_8_suit)
)
) {
# Yep. Move the card to to discard pile.
$hand->give_a_card($self->discard_pile, $card_index);
# Reset the declared suit.
$self->last_8_suit(undef);
# Announce the play to the players.
foreach ($self->players) {
$_->call_ui_function(player_played_card =>
$player->nick,
$card->name . $card->suit);
}
} else {
return(fault=>907, "That's not a legal play!");
}
# Set the current player as the last card-player. This will help in case
# the player later makes a 'change_suit()' call.
$self->last_card_player($player);
# See if the game is over.
return if $self->check_for_game_over;
# Move the turn along, unless an 8 was played. In that case,
# we must wait for the player to choose a suit first.
unless ($card->name eq '8') {
$self->end_turn;
}
return;
}
sub rpc_choose_suit {
my $self = shift;
my ($player, $suit) = @_;
unless ($player eq $self->current_player) {
return(fault=>901, "It isn't your turn.");
}
unless (
(defined($self->last_card_player)) and
($self->last_card->name eq '8') and
($self->last_card_player eq $player)
) {
return(fault=>905, "You can't change the suit now!");
}
my $suit_letter = uc(substr($suit, 0, 1));
if (grep($suit_letter eq $_, qw(D S H C))) {
$self->call_ui_function_on_everyone(player_chose_suit=>$player->nick, $suit);
$self->last_8_suit($suit_letter);
$self->end_turn;
} else {
return(fault=>906, "I can't turn '$suit' into a recognizable suit name.");
}
}
sub rpc_draw_card {
my $self = shift;
my ($player) = @_;
unless ($player eq $self->current_player) {
return(fault=>901, "It isn't your turn.");
}
unless (@{$self->deck->cards}) {
return(fault=>904, "You can't draw a card; the draw pile has no cards left!");
}
my $drawn_card_name = $self->deck->top_card->name . $self->deck->top_card->suit;
$self->deck->give_cards($player->hand, 1);
# Tell the player about this new card.
$player->call_ui_function(draw_card=>$drawn_card_name);
# Tell the other players that the player drew a card.
map($_->call_ui_function(player_drew_card=>$player->nick), grep($_ ne $player, $self->players));
}
################
# Internal Methods
################
sub deal_cards {
my $self = shift;
# Two-player games have seven-card hands; others get five-card hands.
my $hand_size = scalar($self->players) == 2? 7: 5;
for my $player ($self->players) {
$self->deck->give_cards($player->hand, $hand_size);
my @cardz = map($_->name . $_->suit, @{$player->hand->cards});
$player->call_ui_function(receive_hand=>\@cardz);
}
}
sub flip_starter {
my $self = shift;
$self->deck->give_cards($self->discard_pile, 1);
my $starter_name = $self->discard_pile->top_card->name . $self->discard_pile->top_card->suit;
$self->call_ui_function_on_everyone(starter_card=>$starter_name);
# Semi-hack... if the starter is an 8, silently set our notion of
# the last 8 suit. This keeps the logic in the match-checking part of
# rpc_play_card simpler.
$self->last_8_suit = $self->last_card->suit if $self->last_card->name eq '8';
}
sub last_card {
my $self = shift;
return $self->discard_pile->top_card;
}
sub end_turn {
my $self = shift;
$self->rotate_current_player;
$self->call_ui_function_on_everyone(start_turn=>$self->current_player->nick);
$self->make_player_draw;
}
# make_player_draw: Called at the start of a turn. Has the player draw
# cards from the deck until a play becomes possible. (This will draw
# no cards if the player starts the turn with legal plays already
# available.)
sub make_player_draw {
my $self = shift;
my $player = $self->current_player;
my @hand_cards = @{$player->hand->cards};
my $last_card = $self->last_card;
my $last_value = $last_card->name;
my $last_suit = defined($self->last_8_suit)?
$self->last_8_suit : $last_card->suit;
my $last_8_suit = $self->last_8_suit;
until (
grep(
(
($_->name eq 8) or
($_->suit eq $last_suit) or
($_->name eq $last_value)
),
@hand_cards,
) or
@{$self->deck->cards} == 0
) {
if (@{$self->deck->cards} == 0) {
# Ye gods, the draw pile has run out. This player's turn is over.
$self->call_ui_function_on_everyone(player_passes=>$player->nick);
$self->end_turn;
}
my $drawn_card_name = $self->deck->top_card->name . $self->deck->top_card->suit;
$self->deck->give_cards($player->hand, 1);
@hand_cards = @{$player->hand->cards};
$player->call_ui_function(draw_card=>$drawn_card_name);
# Tell the other players that the player drew a card.
map($_->call_ui_function(player_drew_card=>$player->nick), grep($_ ne $player, $self->players));
}
}
sub check_for_game_over {
my $self = shift;
my $hand = $self->current_player->hand;
unless ($hand->cards->[0]) {
# The current player has no cards. Game over!
# Sort the players into a Frivolity winner list.
# Each member of this list is a listref, containing all the players
# tied for that place. (This will often be just a single player, if
# there are no actual ties for that place.)
my %players_by_hand_value;
my %scores_by_nickname;
for my $player ($self->players) {
my $hand_value = $player->evaluate_hand;
$scores_by_nickname{$player->nick} = $hand_value;
if ($players_by_hand_value{$hand_value}) {
push (@{$players_by_hand_value{$hand_value}}, $player);
} else {
$players_by_hand_value{$hand_value} = [$player];
}
}
my @winners; # Sorted winner list!
# We will add players to the winner list according to their hand values.
# A simple sort by these values will give us the right order, since
# the fewer points, the better you did. (The first-place winner has zero
# points!)
foreach (sort(keys(%players_by_hand_value))) {
push (@winners, $players_by_hand_value{$_});
}
# The the players about this.
$self->call_ui_function_on_everyone(scores=>\%scores_by_nickname);
# Set this array as my winner list, and signal that we're all done,
# by calling the end_game() method.
# The ref will take care of the rest!
$self->winners(@winners);
$self->end_game;
return 1;
} else {
# Else, the player still holds cards, so the game ain't over yet...
return 0;
}
}
################
# Player Class
################
package Volity::Player::CrazyEights;
use warnings;
use strict;
use base qw(Volity::Player);
use fields qw(hand score);
# evaluate_hand: Return the game-over score of this player's hand.
sub evaluate_hand {
my $self = shift;
my $score = 0;
my @cards = @{$self->hand->cards};
for my $card (@cards) {
$score += $card->value;
}
return $score;
}
1;
Presumably you'd want to add some POD documentation too, but this code listing is already long enough.
Warning
For the remainder of this article, things are going to get a little bit unsure, mushy, and generally handwringy. While the core Volity concepts (and Frivolity's implementation of them) have received a lot of thought and work, the finer points of handling UI files and actually running game servers haven't enjoyed this attention. Blunt ways exist to handle both tasks, and are described here, but the human interface to both is subject to change. (In fact, it had better, since it's not very good right now.)
Future editions of this document will describe these processes in greater detail as they become more detailed themselves. For now, you're going to see sheepish phrases like "at the time of this writing" and "for now" quite a bit.
Once you've written a game module, you can immediately try running it within a game server, as described in the section called “Running your game”. Before you (or anyone else) can actually enjoy the game, however, at least one UI file must exist for its ruleset.
Each Volity UI file is either a simple ECMAScript document, or some other kind of document (perhaps XML) containing embedded ECMAScript. It defines a collection of ECMAScript objects and methods that meet the needs of a single intersection of ruleset and client type. For example, one UI file might allow users of text clients to play chess, while another lets SVG-clients play chess, and a third is meant for text clients wishing to play poker.
At this time, Volity lacks a standard way to let a UI file indentify the ruleset and client type at which it aims itself, and there's also no agreed-upon method to get these files into the hands of users. The core development team hopes to hash out these crucial topics in mid-2004.
Volity UI files should adhere to a a certain code protocol in order to work correctly. Volity client programs hand them a few predefined ECMAScript objects when the game begins, and expect that it defines at least a couple of globally standard methods in order to start and stop the game properly. Furthermore, the ruleset will have method-definition expectations of its own.
Volity client applications, upon starting a new game, instance an internal ECMAScript interpreter, and then define a handful of objects with standard names, properties, and methods. The game's UI file may use these objects however it likes, and in some cases must do so in order to define the UI protocol's few required methods.
- game
This object primarily represents incoming RPC-based communicaion from the referee. The UI file must define one method for its game object for every function named in its ruleset's referee-to-player API.
A UI file must also define game.start_game() and game.end_game(), which set up and cleanup the playing area, respectively.
- client
The client application uses this object as the principal communication conduit between itself and the UI files it loads. By making method calls on it, a UI file can change the client's behavior in ways that don't have to do with the referee, such as making game-specific commands available to the human user. These commands can then trigger methods defined by the UI file, themselves also attached to this client object.
- info
This object simply holds some predefined properties containing some information about the game in progress. Salient fields include info.nickname, which contains the player's own nickname for this game, and info.opponents, a hash whose keys are the nicknames of the other people at the table, and whose values are initially undefined; the file can later write interesting tidbits about opponents here, such as their scores, or their visible hand sizes.
Your game module is complete — well, it compiles, at least — and you've either obtained or created a UI file that works with your favorite Volity client. Finally, you can try running your module within a game server, and playing the resulting game!
Well, that's a tad optimistic. More likely, you will instead note the way that your game fails, and then tinker with the module code (or the UI file, if you are its author) until you acutally can play the game. The point is, you now have all the tools you need to enter this last leg of Volity game development, so let's dive in.
If you haven't already done so, you must create a Jabber account for your game server to use, and then inform the Volity bookkeeper about your server's existence. You can fulfill both of these tasks at once by visiting the registration tools at the volity.net website.
To make your game actually run, launch the server.pl script that ships with the Frivolity software, supplying your module's classname as an argument. You will need to supply additional arguments to specify important connection information (such as your server's Jabber account name), as detailed in the script's manpage.
Once the server successfully launches, it runs as a "headless", noninteractive process until it terminates, either through user intervention (i.e. you send it a kill signal) or because it crashed (in which case you should notify me).
This, combined with the fact that the script can be configured only through a pile of command-line switches at launch-time, and features no real logging of any kind, rather gives one the impression that the Volity team hasn't spent much time developing the server executable yet, eh? Fear not; if you're reading these words, you're either an early adopter who can ask the core designers for personal help getting your game to work, or you're reading some hideously outdated version of this document and should go check for a more recent version. In either case, you can expect to see a much friendlier server interface in the near future.
In the meantime, please keep an eye on http://volity.org for developer updates, contact me with any questions and comments about this article or Volity, and happy hacking!
[1] The referee, of course, being the automated network entity that hosts an individual game instance. Refer to the Volity overview article for more details.
[2] The bookkeeper, another automated network entity, is responsible for (among other things) keeping track of all the network's game servers, and answering players' queries about them. Again, see the overview essay for more information.
[3] Ruleset URI: http://volity.org/games/rps. All the URIs connected to real-world examples in this article also happen to function as URLs, pointing to Web pages that describe their relative rulesets in detail. Feel free to look them up.
[4] Yours truly.
[5] Volity culture might eventually declare small protocols the Right Thing to Do, but Volity culture is rather too young at the time of this writing to be making any such statements. On the other hand, there is a lot to be said about the benefits of small, simple protocols and APIs...
[6] You don't have to do it this way, of course; if a stand-alone module file that defines a player subclass is visible in your Perl's @INC path, you can use it in your game. But since player classes are often explicitly tied to game implementations, and useful only in that context, it's often handy to keep the two together in the same Perl module file.
[7] Which are, under the hood, simply members of the pseudohash that is the game object. At the time of this writing (mid-2004), Perl has mixed feelings about pseudohashes; as of version 5.8.3, Perl actually generates deprecation warnings upon their use, which is why the base Frivolity code contains no warnings qw(deprecated); statements. However, nothing in the code literally refers to pseudohashes, since it created them through the fields::new() function, and refers to the resulting objects as ordinary hashes thereafter. It is my hope that Perl will silently shift its guts so that the fields stuff uses (the conceptualized but not yet implemented) restricted hashes in a future version. In the meantime, everything Just Works for now, and you shouldn't worry too much about it. I hope.
[8] These fields, and the accessor methods you use, are implemented through the Class::Data::Inheritable Perl module. But you don't need to know that in order to use the accessors.
[9] Available from CPAN. If you haven't used CPAN to download and install Perl modules, visit http://cpan.org, or read the CPAN manpage. Or just ask me for help.
