Or: use OSGi to write a dueling card game - part 4
In the previous postings I created the basics of a dueling card game using OSGi services. In this last posting of this series I'm going to add some special spell cards. Finally I'll use the capabilities of OSGi to dynamically add a bundle with new cards to the ongoing games without having to interrupt them.
Adding Spell Cards
Spells make the game really interesting as they can influence the game beyond the standard rules. They can do things like change the power of a monster, inflict damage on a player or another card or generally change the game state. Sometimes spells can behave like monsters or monsters can have additional spell-like behaviour too. My SpellDeck bundle adds a number of them, each of which has special abilities and a special implementation.
In my Activator, I'm registering my spell cards (5 of each):
public class Activator implements BundleActivator {
public void start(BundleContext ctx) throws Exception {
for (int i = 0; i < 5; i++) {
registerCard(ctx, new ThunderBoltCard());
registerCard(ctx, new ReflectCard());
registerCard(ctx, new SkipBattlePhaseCard());
registerCard(ctx, new DoubleAttackCard());
registerCard(ctx, new MedicinCard());
registerCard(ctx, new ExplosionCard());
registerCard(ctx, new LightningBoltCard());
registerCard(ctx, new DoubleStrengthCard());
registerCard(ctx, new HalfStrengthCard());
}
}
private void registerCard(BundleContext ctx, Card c) {
ctx.registerService(Card.class.getName(), c,
new Hashtable());
}
Let's zoom in on a few of them: ThunderBoltCard, SkipBattlePhaseCard, ReflectCard and DoubleStrengthCard.
ThunderBoltCard
This spell card can be played during a player's Standby or Recover phase and deals 20 damage directly to the opponent. This functionality is implemented in the execute() of this Command Card.
public class ThunderBoltCard extends AbstractCommandCard implements Card, Command {
public ThunderBoltCard() {
super(CardType.SPELL, "Thunderbolt",
"Thunderbolt deals 20 damage to your opponent.");
}
public boolean execute(Game game) {
Player p = game.toGrave(this); // discard this card
Player q = game.getOtherPlayer();
q.setScore(q.getScore() - 20);
printBoth(game, p + " plays Thunderbolt, " +
q + " loses 20 life");
return true;
}
}
SkipBattlePhaseCard
This card can be played by a player who is being attacked during battle. It will end the current Battle phase aborting any pending attacks and move to the Recover phase. Here's the code:
public class SkipBattlePhaseCard extends AbstractCommandCard implements Card, Command {
protected SkipBattlePhaseCard() {
super(CardType.SPELL, "Battle Skipper",
"With battle skipper, your opponent must drop " +
"the current attack and skip the battle phase.");
}
public boolean execute(Game game) {
Player p = game.getOwner(this);
if (game.getPlayer() == p) {
p.getUI().print("Can only play this card during " +
"your opponent's battle phase when attacked.");
return false;
} else {
game.setAttacking(new ArrayList<Fighter>());
game.setPhase(Phase.RECOVER);
game.toGrave(this);
printBoth(game, p + " plays Battle Skipper, " +
which causes the current battle phase to end.");
return true;
}
}
}
ReflectCard
This spell can also be played by a player who is being attacked during battle. It will reflect the same amount of defense back to the attacker as was the original attack. Typically this will destroy the attacking monster.
public class ReflectCard extends AbstractCard implements Card, Fighter {
private int defense;
public ReflectCard() {
super(CardType.SPELL, "Reflect",
"Reflects an attack back to the attacker as " +
"defense, will normally destroy the attacker.");
}
public void damage(Game game, int amount) {}
public int getAttack() {
return 0;
}
public int getDefense() {
return defense;
}
public boolean responding(Game game) {
defense = 0;
// calculate the defense
for (Fighter f : game.getAttacking()) {
defense += game.getAttack(f);
}
return true;
}
}
DoubleStrengthCard
This is an attached card, which means that it is attached to another Fighter card on the field. It doubles the attack of the fighter card. The attachment behaviour of this card is implemented in the AbstractAttachedCard base class. This card implements the Modifier interface, which is taken into account when a monster's strength is calculated.
public class DoubleStrengthCard extends AbstractAttachedCard implements Card, Modifier {
protected DoubleStrengthCard() {
// This card can be attached to a Monster card on
// the table of type Fighter
super("Double Strength",
Area.TABLE, Fighter.class, CardType.MONSTER,
"Target creature gets double the attack.");
}
public int addToAttack(Game game, Fighter fighter) {
if (fighter instanceof Card) {
Player owner = game.getOwner((Card) fighter);
if (owner != null &&
owner.getTable().contains(fighter)) {
return fighter.getAttack();
}
}
return 0;
}
public int addToDefense(Game game, Fighter fighter) {
return 0;
}
}
Get the code of the other spells by checking out the SpellDeck Bundle from the usual location: http://coderthoughts.googlecode.com/svn/trunk/duel/.
Adding More Monsters while playing
Last but not least, I'm going to add a whole bunch of extra monsters to my game, and I'm going to do this while a game is being played. In a development scenario this is not really normal, but it will be needed once our application is successful and in production. Let's say our application is used by many users and we don't want to disturb our users by the mere action of adding a few new cards to the game. We just want those cards to appear in any ongoing games and in any new games from now on. This is where OSGi will really help us. It allows us to add and remove bundles without the need to restart the container. If those new bundles contain new Card service my game controller will be notified of them straight away. Similarly if a bundle that contains cards is being removed, we will be told about it too. If our application (bundles) interact with other bundles via OSGi Services, we will be able to patch our entire application without taking it down. To show this functionality in action I'm going to add the MoreMonsters bundle to the OSGi runtime after the game has started, but first of all I need to slightly modify the addCard() method in my Controller class to shuffle newly arrived cards into the decks of any ongoing games.
public void addCard(Card card) {
synchronized (cards) {
cards.add(card);
// add the new card to any game deck in
// progress in a random place
for (Game g : games) {
System.out.println(
"Shuffling new card into existing game " + card);
List<card> deck = g.getDeck();
deck.add(rand.nextInt(deck.size()), card);
}
}
}
To get started, first check out the MoreMonsters bundle from the usual SVN location: http://coderthoughts.googlecode.com/svn/trunk/duel. This bundle only contains an Activator in which additional monsters (instances of the MonsterCard) are created and registered as Card services (courtesy to my 9 and 11 year old kids, who created these monsters).
I want the MoreMonsters bundle to be added while the game is in progress, so I need to make sure that it's not automatically started with the other bundles. For this, the launch confiration that I'm using in Eclipse needs to be tweaked, I'm setting the 'Auto-Start' of MoreMonsters to false:
You may also need to select the 'Clear the configuration area before launching' option in the 'Settings' tab of the launch configuration, because Equinox remembers your first settings and sometimes doesn't pick up changes like this without clearning the configuration area.
Hit 'Run' and the game will start as usual, with the original three monsters and our spell cards.
If you go into the Equinox console and type 'ss', you'll see that the MoreMonsters bundle is there, but it's just not started:
osgi> ss
Framework is launched.
id State Bundle
0 ACTIVE org.eclipse.osgi_3.3.1.R33x_v20070828
1 ACTIVE org.eclipse.osgi.services_3.1.200.v20070605
3 RESOLVED MoreMonsters_1.0.0
4 ACTIVE DuelInterfaces_1.0.0
7 ACTIVE javax.servlet_2.4.0.v200706111738
8 ACTIVE org.eclipse.equinox.ds_1.0.0.v20070226
9 ACTIVE SpellDeck_1.0.0
10 ACTIVE Duel_1.0.0
11 ACTIVE SimpleUI_1.0.0
12 ACTIVE MonsterDeck_1.0.0
So let's start the bundle!
osgi> start 3
And we'll see our new cards arriving:
Shuffling new card into existing game Thundergirl
Shuffling new card into existing game DB Machine
Shuffling new card into existing game Devil Spider
<...and so on...>
Soon you'll see your new cards arriving in your current game:
In this case the MoreMonsters Bundles was automatically installed by Eclipse for me, it was just not started. A more realistic real-world scenario would be where the OSGi container is running outside of Eclipse. In that case I would first have to install the bundle (Equinox has the install command for that) before I can actually start it.
Dynamically removing services
The same mechanism can be used to dynamically remove services from the system. I'll leave the implementation of that to the reader, but I hope you can see how powerful this mechanism can be. If you need to patch the an OSGi Service-based system, just install the patch bundle first, which will registered the patched services, then remove the old bundle, the old services will be unregistered which can make clients to switch to the patched services. Together with OSGi's strong versioning support, this is a great feature for mission critical systems that need to be up and running 24/7 (like game servers ;).
Equinox Specifics
In this posting I did use some Equinox-specific commands to control the OSGi container, other OSGi containers have other commands for achieving the same. All of the code in these postings should be 100% portable across the various OSGi containers.
Download the code
All the code for all these postings is available under the Apache License in the google code Subversion Repository.
Monday, December 3, 2007
Sunday, December 2, 2007
OSGi Services for Dynamic Applications (III)
Or: use OSGi to write a dueling card game, part 3
Adding Monster cards to the game and a simple UI
In the previous posting I've created the hardest bit of the game. The actual game controller. In this posting I'm going to create a small bundle that contains a few Monster cards and a simple UI bundle so that we can actually start playing.
Creating the Monster Deck bundle
Every card in my Dueling game is an OSGi Service.
In the Duel Bundle I used OSGi Declarative Services to activate the bundle and consume the services, and you can use DS also to register services, but in this case I'm going to use the plain OSGi API's for this. Registering a service is so simple in OSGi that in this case I'm just going to go API.
My monster cards instances are realized by a class called MonsterCard, which implements the Card, Fighter and Command interfaces. It's nice and simple, also because it extends AbstractCommandCard, which means that Command.execute() for the Standby and Recover phase are implemented as placing the card on the table. Other basic Card requirements such as accessors to the name, ID and description and proper implementations of equals(), hashcode() and toString() are handled by AbstractCard, the superclass of AbstractCommandCard. My MonsterCard class basically adds the implementation of the Fighter interface into the mix. It also specifies when the card is enabled in conditions() - in the Standby phase and the Recover phase, and also in the Battle phase, but only when it's on the table.
public class MonsterCard extends AbstractCommandCard implements Fighter, Card, Command {
private int attack;
private int defense;
public MonsterCard(String name, String desc,
int att, int def) {
super(CardType.MONSTER, name, desc);
attack = att;
defense = def;
}
public void damage(Game game, int amount) {
// nothing to do on damage
// the battle code will discard when hit
}
public int getAttack() {
return attack;
}
public int getDefense() {
return defense;
}
public Condition [] conditions() {
return new Condition [] {
new Condition(Phase.STANDBY),
new Condition(Area.TABLE, Phase.BATTLE),
new Condition(Phase.RECOVER)
};
}
public boolean responding(Game game) {
return true; // can respond to any attacker
}
}
To create any straight monster without any special abilities, I can simply use this class. I'm going to create 3 different monster cards, Super Hero, Floppie and Diamond Buster:
new MonsterCard("Super Hero",
"This is the invincible super hero.", 60, 35));
new MonsterCard("Floppie",
"Floppie joppie.", 15, 25));
new MonsterCard("Diamond Buster",
"Diamond buster is as strong as a rock.", 40, 55));
To turn these POJO's into OSGi services, I need to register them as services with the BundleContext.registerService(), defined as:
registerService(java.lang.String clazz,
java.lang.Object service,
java.util.Dictionary properties)
Simply pass in the name of the class as the first argument, the actual object and optionally a map of properties. The properties can be used when selecting services as a service consumer, but I'm not using them here.
So to get 5 of each card registered when the MonsterDeck bundle is started, a small OSGi Activator class does it all:
package monsterdeck;
import game.model.Card;
import java.util.Hashtable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
public void start(BundleContext ctx) throws Exception {
for (int i = 0; i < 5; i++) {
registerCard(ctx, new MonsterCard("Super Hero",
"This is the invincible super hero.", 60, 35));
registerCard(ctx, new MonsterCard("Floppie",
"Floppie joppie.", 15, 25));
registerCard(ctx, new MonsterCard("Diamond Buster",
"As strong as a rock.", 40, 55));
}
}
private void registerCard(BundleContext ctx, Card c) {
ctx.registerService(Card.class.getName(), c,
new Hashtable());
}
public void stop(BundleContext context)
throws Exception { }
}
The only thing left to do it create the bundle-manifest in the META-INF/MANIFEST.MF file:
Bundle-Name: MonsterDeck Bundle
Bundle-SymbolicName: MonsterDeck
Bundle-Version: 1.0.0
Bundle-Activator: monsterdeck.Activator
Import-Package: game.model,
org.osgi.framework
Export-Package: monsterdeck
I have to import the org.osgi.framework package since in my Activator I'm using the BundleActivator and BundleContext classes from this package. I'm exporting the monsterdeck package from this bundle so that I can use the MonsterCard class later in another bundle.
Adding a simple UI
I've tried to create the simplest UI possible. Its written using SWT and simply implements UserInterface. I just wanted to keep this part as simple as possible, but (as always) GUI classes tend to become relatively big. My GUI looks like this:
At the top you see the cards your opponent has on the table.
Below that is a status area where messages are displayed.
Then the cards you have on the table are shown and at the bottom are the cards that you have in your hand.
On the right hand side there is a status panel where each player's life points are shown, the current game phase is displayed and buttons are present to draw a card, end the current phase and take the damage of a fight.
Monster cards are shown red and spell cards are green. To select a card, you simply click on it. Generally only cards and actions that make sense in the current context are enabled. Attacking monsters have their names shown in italics and card descriptions can be read by hovering with the mouse over a card.
The UI bundles comes with a little factory that is launched as soon as the bundles is started. Type your name in the UI Factory and it will create a User Interface for you which is then registered as a UserInterface Service with OSGi.
You can get this UI implementation by checking it out the SimpleUI bundle from the usual SVN location: http://coderthoughts.googlecode.com/svn/trunk/duel/.
Running the game
This is really easy if you're using Eclipse. What you should have is 4 OSGi Bundle Projects:
Select the Run -> Open Run Dialog... menu and double-click on the OSGi Framework item in the tree. This will create a new OSGi launch configuration for you. By default it will have all the OSGi bundles in the system selected, which is waaaay too much, so I normally deselect the root 'Target Platform' node and then click 'Add Required Bundles':
Hit 'Run' and you're off. The UI Controller will show up and after creating two UI's the game will start.
The game controller prints the names of the users as soon as they register their UI services and when two users are in, it will shuffle the cards and start the game. My console shows:
Adding user interface for: Pinky
Adding user interface for: Brain
Shuffling deck...
I can Draw a card:
Play a monster (e.g. Super Hero) on the table:
and engage in battle: my opponent attacks me with Diamond Buster, I can choose to defend with Super Hero, but he will die, alternatively I can take the damage myself and lose life points.
Before I forget: closing all the windows will not actually exit the system. The OSGi container will keep running. The command to stop the OSGi container is implementation-specific, if you're using Equinox just type in exit in the console.
With only 3 different cards in the game it is a little bit boring to say the least. In the next posting I'll add some cool spell cards and a bunch more monsters too!
Adding Monster cards to the game and a simple UI
In the previous posting I've created the hardest bit of the game. The actual game controller. In this posting I'm going to create a small bundle that contains a few Monster cards and a simple UI bundle so that we can actually start playing.
Creating the Monster Deck bundle
Every card in my Dueling game is an OSGi Service.
In the Duel Bundle I used OSGi Declarative Services to activate the bundle and consume the services, and you can use DS also to register services, but in this case I'm going to use the plain OSGi API's for this. Registering a service is so simple in OSGi that in this case I'm just going to go API.
My monster cards instances are realized by a class called MonsterCard, which implements the Card, Fighter and Command interfaces. It's nice and simple, also because it extends AbstractCommandCard, which means that Command.execute() for the Standby and Recover phase are implemented as placing the card on the table. Other basic Card requirements such as accessors to the name, ID and description and proper implementations of equals(), hashcode() and toString() are handled by AbstractCard, the superclass of AbstractCommandCard. My MonsterCard class basically adds the implementation of the Fighter interface into the mix. It also specifies when the card is enabled in conditions() - in the Standby phase and the Recover phase, and also in the Battle phase, but only when it's on the table.
public class MonsterCard extends AbstractCommandCard implements Fighter, Card, Command {
private int attack;
private int defense;
public MonsterCard(String name, String desc,
int att, int def) {
super(CardType.MONSTER, name, desc);
attack = att;
defense = def;
}
public void damage(Game game, int amount) {
// nothing to do on damage
// the battle code will discard when hit
}
public int getAttack() {
return attack;
}
public int getDefense() {
return defense;
}
public Condition [] conditions() {
return new Condition [] {
new Condition(Phase.STANDBY),
new Condition(Area.TABLE, Phase.BATTLE),
new Condition(Phase.RECOVER)
};
}
public boolean responding(Game game) {
return true; // can respond to any attacker
}
}
To create any straight monster without any special abilities, I can simply use this class. I'm going to create 3 different monster cards, Super Hero, Floppie and Diamond Buster:
new MonsterCard("Super Hero",
"This is the invincible super hero.", 60, 35));
new MonsterCard("Floppie",
"Floppie joppie.", 15, 25));
new MonsterCard("Diamond Buster",
"Diamond buster is as strong as a rock.", 40, 55));
To turn these POJO's into OSGi services, I need to register them as services with the BundleContext.registerService(), defined as:
registerService(java.lang.String clazz,
java.lang.Object service,
java.util.Dictionary properties)
Simply pass in the name of the class as the first argument, the actual object and optionally a map of properties. The properties can be used when selecting services as a service consumer, but I'm not using them here.
So to get 5 of each card registered when the MonsterDeck bundle is started, a small OSGi Activator class does it all:
package monsterdeck;
import game.model.Card;
import java.util.Hashtable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
public void start(BundleContext ctx) throws Exception {
for (int i = 0; i < 5; i++) {
registerCard(ctx, new MonsterCard("Super Hero",
"This is the invincible super hero.", 60, 35));
registerCard(ctx, new MonsterCard("Floppie",
"Floppie joppie.", 15, 25));
registerCard(ctx, new MonsterCard("Diamond Buster",
"As strong as a rock.", 40, 55));
}
}
private void registerCard(BundleContext ctx, Card c) {
ctx.registerService(Card.class.getName(), c,
new Hashtable());
}
public void stop(BundleContext context)
throws Exception { }
}
The only thing left to do it create the bundle-manifest in the META-INF/MANIFEST.MF file:
Bundle-Name: MonsterDeck Bundle
Bundle-SymbolicName: MonsterDeck
Bundle-Version: 1.0.0
Bundle-Activator: monsterdeck.Activator
Import-Package: game.model,
org.osgi.framework
Export-Package: monsterdeck
I have to import the org.osgi.framework package since in my Activator I'm using the BundleActivator and BundleContext classes from this package. I'm exporting the monsterdeck package from this bundle so that I can use the MonsterCard class later in another bundle.
Adding a simple UI
I've tried to create the simplest UI possible. Its written using SWT and simply implements UserInterface. I just wanted to keep this part as simple as possible, but (as always) GUI classes tend to become relatively big. My GUI looks like this:
At the top you see the cards your opponent has on the table.
Below that is a status area where messages are displayed.
Then the cards you have on the table are shown and at the bottom are the cards that you have in your hand.
On the right hand side there is a status panel where each player's life points are shown, the current game phase is displayed and buttons are present to draw a card, end the current phase and take the damage of a fight.
Monster cards are shown red and spell cards are green. To select a card, you simply click on it. Generally only cards and actions that make sense in the current context are enabled. Attacking monsters have their names shown in italics and card descriptions can be read by hovering with the mouse over a card.
The UI bundles comes with a little factory that is launched as soon as the bundles is started. Type your name in the UI Factory and it will create a User Interface for you which is then registered as a UserInterface Service with OSGi.
You can get this UI implementation by checking it out the SimpleUI bundle from the usual SVN location: http://coderthoughts.googlecode.com/svn/trunk/duel/.
Running the game
This is really easy if you're using Eclipse. What you should have is 4 OSGi Bundle Projects:
Select the Run -> Open Run Dialog... menu and double-click on the OSGi Framework item in the tree. This will create a new OSGi launch configuration for you. By default it will have all the OSGi bundles in the system selected, which is waaaay too much, so I normally deselect the root 'Target Platform' node and then click 'Add Required Bundles':
Hit 'Run' and you're off. The UI Controller will show up and after creating two UI's the game will start.
The game controller prints the names of the users as soon as they register their UI services and when two users are in, it will shuffle the cards and start the game. My console shows:
Adding user interface for: Pinky
Adding user interface for: Brain
Shuffling deck...
I can Draw a card:
Play a monster (e.g. Super Hero) on the table:
and engage in battle: my opponent attacks me with Diamond Buster, I can choose to defend with Super Hero, but he will die, alternatively I can take the damage myself and lose life points.
Before I forget: closing all the windows will not actually exit the system. The OSGi container will keep running. The command to stop the OSGi container is implementation-specific, if you're using Equinox just type in exit in the console.
With only 3 different cards in the game it is a little bit boring to say the least. In the next posting I'll add some cool spell cards and a bunch more monsters too!
Subscribe to:
Posts (Atom)