It's a nice thing to get everything right the first time. I think I have many things working already, among them are characteristic-setting effects, replacement effects, skipping turns, taking extra turns, and more than two players.
But what I don't have is a game, which is rather disappointing. Sometimes, I think "okay I want to really try it out, a real-world situation. just add in the glue that's missing and make a simple GUI", and then I realize how much of the glue is missing.
Even though I have effects, I have neither spells nor abilities. I can't play cards or abilities. I have (vanilla) creatures, but no combat. I haven't even tested zones, not to mention the Stack.
Okay, now that I finished whining, let me say that I'm still on this. I'm not giving up. The last few days, I focused on the tight core of magic. Turn order is done, all with phases, steps and priority. Next comes the zones, excluding the Stack for now. When I'm done with that, I'll try to program playing a land, which is drastically different from playing spells or abilities.
This should be enough "core engine" to let me finally play a game. Draw a card, play a land or not, pass priority back and forth until it's the opponent's turn (myself again, no AI yet), and so forth.
Of course, there are needs besides the core engine. Most obviously, a GUI. The other two that come to my mind are decks, which i have started already, and a "controller". LaternaMagica has a Player class, which handles life points and so on, but there's no way yet to to make decisions. But that's another story ;)
Quick follow-up: an important thing I've forgotten for my to-do list is state-based actions. it would be too bad if a player doesn't die when he has no life (however that should happen without creatures or spells...)
Tuesday, March 30, 2010
Friday, March 26, 2010
Undo ... and replacement effects!
I finally got time to work on Laterna Magica again. It wasn't that exciting, just adding undo support to all the classes that already existed. While I browsed the classes and looked for actions that I hadn't revised yet, I found the LifeTotal class. Its job is easy: store the number of life points for a player, and whenever it changes, inform listeners.
...except that there are replacement effects. Only because I want to change life points, that doesn't mean it happens! what could be done?
Surprisingly, the solution looks very similar to my undo engine: Every modification is encapsulated into an edit and stored in the engine. If you want to undo something, the undo engine knows what happened, and every single edit knows how to revert itself. For the caller, it's only addListener(myListener), but inside there's new AddListenerEdit(myListener).execute().
Similarly, the replacement engine is the authority for performing replaceable actions, like gaining life. Calling gainLife(amount) really means new LifeEvent(amount).execute().
Execute uses the replacement engine to apply all replacement effects. Those may replace the event with another event, until all are through. The resulting event is then really executed (the implementation method is called execute0).
Edit and ReplaceableEvent are not connected. Both have an execute method, but that's more or less coincidence. Logically, there is a connection. Look at execute0 from LifeEvent:
public void execute0() {
((LifeTotalImpl) getPlayer().getLifeTotal()).fireLifeEvent(this);
}
//...and from LifeTotalImpl
void fireLifeEvent(LifeEvent e) {
CompoundEdit ed = new CompoundEdit(getGame(), true, "Adjust " + getPlayer() + "'s life by "
+ (e.isGained()? "+":"-") + e.getAmount());
new AdjustLifeEdit(e.getChange()).execute();
// Guaranteed to return a non-null array
Object[] l = listeners.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
boolean gain = e.isGained();
for(int i = l.length - 2; i >= 0; i -= 2) {
if(l[i] == LifeListener.class) {
if(gain) ((LifeListener) l[i + 1]).lifeGained(e);
else ((LifeListener) l[i + 1]).lifeLost(e);
}
}
ed.end();
}
Two things are of importance here: First, changing the life is done through an edit, so it can be undone. Second, informing the listeners is encapsulated ina compound edit, so that everything related to this event is grouped together.
I'm quite proud about this piece of code; I expected replacement effects to be very hard, but as it turns out, if there are no flaws in this concept, they should be quite easy.
...except that there are replacement effects. Only because I want to change life points, that doesn't mean it happens! what could be done?
Surprisingly, the solution looks very similar to my undo engine: Every modification is encapsulated into an edit and stored in the engine. If you want to undo something, the undo engine knows what happened, and every single edit knows how to revert itself. For the caller, it's only addListener(myListener), but inside there's new AddListenerEdit(myListener).execute().
Similarly, the replacement engine is the authority for performing replaceable actions, like gaining life. Calling gainLife(amount) really means new LifeEvent(amount).execute().
Execute uses the replacement engine to apply all replacement effects. Those may replace the event with another event, until all are through. The resulting event is then really executed (the implementation method is called execute0).
Edit and ReplaceableEvent are not connected. Both have an execute method, but that's more or less coincidence. Logically, there is a connection. Look at execute0 from LifeEvent:
public void execute0() {
((LifeTotalImpl) getPlayer().getLifeTotal()).fireLifeEvent(this);
}
//...and from LifeTotalImpl
void fireLifeEvent(LifeEvent e) {
CompoundEdit ed = new CompoundEdit(getGame(), true, "Adjust " + getPlayer() + "'s life by "
+ (e.isGained()? "+":"-") + e.getAmount());
new AdjustLifeEdit(e.getChange()).execute();
// Guaranteed to return a non-null array
Object[] l = listeners.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
boolean gain = e.isGained();
for(int i = l.length - 2; i >= 0; i -= 2) {
if(l[i] == LifeListener.class) {
if(gain) ((LifeListener) l[i + 1]).lifeGained(e);
else ((LifeListener) l[i + 1]).lifeLost(e);
}
}
ed.end();
}
Two things are of importance here: First, changing the life is done through an edit, so it can be undone. Second, informing the listeners is encapsulated ina compound edit, so that everything related to this event is grouped together.
I'm quite proud about this piece of code; I expected replacement effects to be very hard, but as it turns out, if there are no flaws in this concept, they should be quite easy.
Wednesday, March 24, 2010
Progress: LaternaCommon
The second thing I've done in the last months is LaternaCommon. It is more or less a collection of useful utilities:
You can use this, too, for free. However, I haven't yet uploaded a jar'ed version of it.
Okay, that's for now, and don't worry, in the last few days I resumed development of the core program!
- It provides a single, application-wide TreeProperties object, loaded from a configurable location
- Configures log4j from a properties file, either configured directly, or by naming a property which stores the location
- Provides an error-handling dialog for uncaught exceptions or to invoke manually
- last but not least, it has a tool that can automatically unzip a file inside the jar. this is very useful to init an application on its first launch.
With this utility, you store a regular zip file inside the jar, and configure a destination directory and a check file on the file system. If the check file is not found on launch, the zip file is unpacked to the specified directory. Any number of zip files can be specified this way.
You can use this, too, for free. However, I haven't yet uploaded a jar'ed version of it.
Okay, that's for now, and don't worry, in the last few days I resumed development of the core program!
Sunday, March 21, 2010
Progress: TreeProperties
I just realized that I didn't post anything for about two months. Well, I did do something, and I want to keep you up.
I have to say that code versioning systems like SVN are a great thing. Besides having a backup on the internet, I'm able to trace back my actions in the last two months. You can look here anytime to see what I was coding recently.
My task was to rework the TreeProperties system that i have come up with sometimes. The point behind it is that you can store properties in simple text files and, for example, implement localization this way. Well, java has something like that built in, but my Library allows much more, the biggest pro being that properties can be split over several files and keys are hierarchical. You can read more about - and download - it here.
Then was a long pause, I took a vacation and went skiing. After that, work went on, and I added many features, fixed bugs etc. In a nutshell, what the library is able to do is:
Next time is about the new sibling of the LaternaMagica project, LaternaCommon, which adds even another level of convenience for the developer in the areas of logging, error handling and deployment.
I have to say that code versioning systems like SVN are a great thing. Besides having a backup on the internet, I'm able to trace back my actions in the last two months. You can look here anytime to see what I was coding recently.
My task was to rework the TreeProperties system that i have come up with sometimes. The point behind it is that you can store properties in simple text files and, for example, implement localization this way. Well, java has something like that built in, but my Library allows much more, the biggest pro being that properties can be split over several files and keys are hierarchical. You can read more about - and download - it here.
Then was a long pause, I took a vacation and went skiing. After that, work went on, and I added many features, fixed bugs etc. In a nutshell, what the library is able to do is:
- Store properties in a tree structure of multiple files
A file can reference another file, which is then parsed in the same way. Thus, any structure of files may be used to realize the needed properties. - Use hierarchical property-keys, like "/lang/en/save"
The namespace mechanism is independent of the file structure, i.e. not bound to a directory structure. When a file is referenced, it inherits its referrer's namespace. For example, if you reference a file with the key "/lang/en", its "save" property has the full key "/lang/en/save". However, if you prefer, you can reference the file with the empty key and literally use "/lang/en/save" in the included file for the same result. - Support for many data types
Unlike the java Properties class, which only supports unparsed string values, TreeProperties supports many data types out of the box - no more casting and parsing when accessing properties! Data types include all of the java primitive types, String, BigInteger and BigDecimal, Date and Calendar, File, Path and URL. The mechanism of data types is easily extendable. It requires implementing one interface and registering the type - that's it!
Special attention goes to the Path type - it adds an abstraction to regular java files that allows to use system properties to be substituted in the path. for example, the values "~/file.txt" or "${user.home}/file.txt", interpreted using the path type, both represent the file "file.txt" in the executing user's home directory. Including files (see above) uses the path type to parse the include, therefore the included file may be personalized for each user, allowing him to set preferences without annoying other users. - Simple access to the API
Most needs don't go beyond reading and writing existing properties. Therefore there's a class for exactly that - it has convenience methods that return the standard types already cast, allows default values, and supports localization. The only reason to read into the API is if you need to store new properties. - Easy GUI
TreeProperties is a background utility that makes a developer's life way easier. However, now and then there's a case when it steps into the foreground. If you want to show a GUI to the user to let him edit the values, don't build it yourself. It's already there! Setting up a GUI with labels and input components to allow the user to edit properties requires a single method call. what you get is a fully layouted swing panel, ready to add to your GUI. The input components are adjusted for the data type - (formatted) text fields, checkboxes and dropdown fields - and initialized to the current value. Changes to the inputs write to the properties object and are therefore available immediately.
Next time is about the new sibling of the LaternaMagica project, LaternaCommon, which adds even another level of convenience for the developer in the areas of logging, error handling and deployment.
Sunday, January 24, 2010
Implementing Undo: A working approach
When I realized that the need for an extra parameter really hurts the usability, I had to rethink the way of storing moves. Additionally, moves should be executed immediately, and not simply stored and scheduled for later execution, to make the semantics feel more natural.
By the way, I changed the name from Move to Edit, since that is the classic name for undoable stuff. Not that it would matter too much...
I didn't want to give up the tree, but needed a way to get the parent without using a parameter. Gladly, I found a solution that makes it pretty easy to create the right hierarchy. In a nutshell, the node that is currently appended to is stored in the controller, and when a node is created, it automatically finds the controller that belongs to the game.
This way, I was able to keep my tree structure and only had to do minimal changes. First, I had to change the Edit constructor to add itself to the controller. Second, the controller needed some notion of the current parent node.
Well, there were some problems on the way... I wanted to automatically execute edits at creation. the problem is the order of initialization. Imagine this:
abstract class Edit {
public Move() {
//add to the controller...
//then:
execute();
}
public abstract void execute();
public abstract void rollback();
}
class MyEdit extends Edit {
private Object importantValue;
public MyEdit(Object importantValue) {
super();
this.importantValue = importantValue;
}
public abstract void execute() {
//do something with importantValue
}
public abstract void rollback() {
//undo something with importantValue
}
}
execute is called in the Edit Constructor, which is called at the beginning of initialization, before importantValue is set! Although it's logical that the superclass must be initialized first - starting with Object, which reserves memory on the heap to store atributes in the first place - this leads to the requirement for some annoying workarounds. For this, it's just to move the execute out of the constructor - it is essentially done after constructing any move. I will show another workaround later - for the random number generator - until then, my new code is online; i had a hard time with subclipse, but everything's alright now. feel free to peek into laterna.magica.edit, and laterna.magica.timestamp; yet the only classes to use this new development
By the way, I changed the name from Move to Edit, since that is the classic name for undoable stuff. Not that it would matter too much...
I didn't want to give up the tree, but needed a way to get the parent without using a parameter. Gladly, I found a solution that makes it pretty easy to create the right hierarchy. In a nutshell, the node that is currently appended to is stored in the controller, and when a node is created, it automatically finds the controller that belongs to the game.
This way, I was able to keep my tree structure and only had to do minimal changes. First, I had to change the Edit constructor to add itself to the controller. Second, the controller needed some notion of the current parent node.
Well, there were some problems on the way... I wanted to automatically execute edits at creation. the problem is the order of initialization. Imagine this:
abstract class Edit {
public Move() {
//add to the controller...
//then:
execute();
}
public abstract void execute();
public abstract void rollback();
}
class MyEdit extends Edit {
private Object importantValue;
public MyEdit(Object importantValue) {
super();
this.importantValue = importantValue;
}
public abstract void execute() {
//do something with importantValue
}
public abstract void rollback() {
//undo something with importantValue
}
}
execute is called in the Edit Constructor, which is called at the beginning of initialization, before importantValue is set! Although it's logical that the superclass must be initialized first - starting with Object, which reserves memory on the heap to store atributes in the first place - this leads to the requirement for some annoying workarounds. For this, it's just to move the execute out of the constructor - it is essentially done after constructing any move. I will show another workaround later - for the random number generator - until then, my new code is online; i had a hard time with subclipse, but everything's alright now. feel free to peek into laterna.magica.edit, and laterna.magica.timestamp; yet the only classes to use this new development
Sunday, January 17, 2010
Implementing Undo: Trial and Error
I'm working eagerly on undo, and have a few thoughts to share on it. first, let me say that it's really not easy. so many thing can go wrong, since undo should mean "create a state as if the last action had never occured". it's easy to forget one of the implicit things that happen when you change something. if it's an event listener, or updating some kind of cache, there are so many possibilities.
the first thing to do when you start a programming task is deciding your goals. for undo, I had two goals of great importance
the second should be looking for help and reference material, but I just skipped over to the third one: error ;)
my first thought was a tree of actions with a controller actually executing of them. the controller checks that actions are executed in proper sequence. the work would be to create and append the action, and then let the controller execute it. the parent action is passed as a parameter to the modifying method so it knows where to plug the action into the tree:
//from laterna.magica.timestamp.Timestamp
private TimestampFactory f;
private int timestamp;
...
public void updateTimestamp(Move m) {
//the constructor plugs the move into the tree
//with m as its parent
Move parent = new CompoundMove(m, true);
//plugs in the move which creates the next timestamp
//for the factory
f.nextTimestamp(parent);
//plugs in the move which updates this Timestamp's value
new UpdateTimestampMove(parent);
}
...
private class UpdateTimestampMove extends Move {
private int before;
public UpdateTimestampMove(Move parent) {
//Move does the tree stuff
super(parent);
before = timestamp;
}
@Override
public void make() {
timestamp = f.getCurrentTimestamp();
}
@Override
public void undo() {
timestamp = before;
}
}
//from laterna.magica.timestamp.TimestampFactory
private int currentTimestamp;
...
public int getCurrentTimestamp() {
return currentTimestamp;
}
public void nextTimestamp(Move m) {
new NextTimestampMove(m);
}
...
private class NextTimestampMove extends Move {
private int currentTimestamp;
public NextTimestampMove(Move parent) {
super(parent);
currentTimestamp = TimestampFactory.this.currentTimestamp;
}
@Override
public void make() {
//Set the timestamp after that of this move as current
TimestampFactory.this.currentTimestamp = currentTimestamp + 1;
}
@Override
public void undo() {
//Resets to the current timestamp of this move
TimestampFactory.this.currentTimestamp = currentTimestamp;
}
}
You know what? a call to updateTimestamp() doesn't even really update the timestamp! and even better, it isn't correct just because of that. the line
currentTimestamp = TimestampFactory.this.currentTimestamp;
is executed when the move is constructed. so, if two such moves are constructed before any of them was executed, they bear the same value.
this is known as "lost update": you expect something to be executed in the order read-compute-write-read-compute-write, but really it's read-read-compute-compute-write-write, or something similar. both compute steps can't take into account the other computation, respectively, and the first write step is plainly overwritten.
this is exactly the problem with the code: what you expect (having the new timestamp stored after the call) ist not what the code does, and such misunderstandings are a major source of bugs. additionally, the second point is almost ignored. this layout requires an additional parameter in every method that does the slightest modification, and a manual "execute" in the controller to do the actual work.
impressing how lines fly if you really have something to talk about! i will continue with this story very soon, until then here's a quick lines-of-code count with cloc.pl:
http://cloc.sourceforge.net v 1.08 T=1.0 s (138.0 files/s, 8966.0 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code scale 3rd gen. equiv
-------------------------------------------------------------------------------
Java 138 1497 3126 4343 x 1.36 = 5906.48
Impressing, there are alone 1.5k EMPTY lines in my program ;)
the first thing to do when you start a programming task is deciding your goals. for undo, I had two goals of great importance
- it must be possible to divide big actions into several small ones. ideally, every action is so small that it only changes one value. in other words, an action should be simple enough that you can find almost all bugs
- it should be easy to use the system. magic has many cards, and if the majority of card code is related to the undo system, that can't be good. coding cards should be easy, ideally fun, so that many of them get implemented.
the second should be looking for help and reference material, but I just skipped over to the third one: error ;)
my first thought was a tree of actions with a controller actually executing of them. the controller checks that actions are executed in proper sequence. the work would be to create and append the action, and then let the controller execute it. the parent action is passed as a parameter to the modifying method so it knows where to plug the action into the tree:
//from laterna.magica.timestamp.Timestamp
private TimestampFactory f;
private int timestamp;
...
public void updateTimestamp(Move m) {
//the constructor plugs the move into the tree
//with m as its parent
Move parent = new CompoundMove(m, true);
//plugs in the move which creates the next timestamp
//for the factory
f.nextTimestamp(parent);
//plugs in the move which updates this Timestamp's value
new UpdateTimestampMove(parent);
}
...
private class UpdateTimestampMove extends Move {
private int before;
public UpdateTimestampMove(Move parent) {
//Move does the tree stuff
super(parent);
before = timestamp;
}
@Override
public void make() {
timestamp = f.getCurrentTimestamp();
}
@Override
public void undo() {
timestamp = before;
}
}
//from laterna.magica.timestamp.TimestampFactory
private int currentTimestamp;
...
public int getCurrentTimestamp() {
return currentTimestamp;
}
public void nextTimestamp(Move m) {
new NextTimestampMove(m);
}
...
private class NextTimestampMove extends Move {
private int currentTimestamp;
public NextTimestampMove(Move parent) {
super(parent);
currentTimestamp = TimestampFactory.this.currentTimestamp;
}
@Override
public void make() {
//Set the timestamp after that of this move as current
TimestampFactory.this.currentTimestamp = currentTimestamp + 1;
}
@Override
public void undo() {
//Resets to the current timestamp of this move
TimestampFactory.this.currentTimestamp = currentTimestamp;
}
}
You know what? a call to updateTimestamp() doesn't even really update the timestamp! and even better, it isn't correct just because of that. the line
currentTimestamp = TimestampFactory.this.currentTimestamp;
is executed when the move is constructed. so, if two such moves are constructed before any of them was executed, they bear the same value.
this is known as "lost update": you expect something to be executed in the order read-compute-write-read-compute-write, but really it's read-read-compute-compute-write-write, or something similar. both compute steps can't take into account the other computation, respectively, and the first write step is plainly overwritten.
this is exactly the problem with the code: what you expect (having the new timestamp stored after the call) ist not what the code does, and such misunderstandings are a major source of bugs. additionally, the second point is almost ignored. this layout requires an additional parameter in every method that does the slightest modification, and a manual "execute" in the controller to do the actual work.
impressing how lines fly if you really have something to talk about! i will continue with this story very soon, until then here's a quick lines-of-code count with cloc.pl:
http://cloc.sourceforge.net v 1.08 T=1.0 s (138.0 files/s, 8966.0 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code scale 3rd gen. equiv
-------------------------------------------------------------------------------
Java 138 1497 3126 4343 x 1.36 = 5906.48
Impressing, there are alone 1.5k EMPTY lines in my program ;)
Thursday, January 7, 2010
Complex systems
Previously, I said that business applications were not interesting from a programming perspective. Well, I think this is true in general, but there's probably one big exception: Enterprise Ressource Planning systems. These are pretty complex, allow plugins and customization to a big extent.
Another thing that identifies sophisticated systems is the modelling of not only data but also actions in the software. This is very important in ERP systems, so that decisions are traceable, and the output of employees or departments is visible.
This came to my mind during today's Business Information Management class. It may be a little off-topic, but it's good to see that there's interesting software around besides games.
Another thing that identifies sophisticated systems is the modelling of not only data but also actions in the software. This is very important in ERP systems, so that decisions are traceable, and the output of employees or departments is visible.
This came to my mind during today's Business Information Management class. It may be a little off-topic, but it's good to see that there's interesting software around besides games.
Subscribe to:
Posts (Atom)