what would be the proper oop way to do this?

Started by
17 comments, last by Cornstalks 11 years, 4 months ago
I am not trying to feed the war on OOP, but I've been writing board games for 20 years as a hobby and I can tell you what's useful and what's not. Most of this post is advice about writing an engine for a board game. If you are writing a GUI, performance is probably much less of an issue and you can afford to use whatever design you want.

You basically need two classes when programming a board game: Board and Move. Instead of "Board" you can call the class "Game" or "GameState", since you may want it to remember the history of moves (so they can be undone, or so you can figure out if the current position is a repetition of a previous position). In my own programs I tend to call it "Board", perhaps because I am used to the name.

Using C++ for syntax, Board's interface has methods like:
* int generate_moves(Move *moves) const;
* void make_move(Move const &move);
* bool is_game_over(int &result);

Depending on what algorithm you will use for your AI and how expensive it is to copy a Board, you may want to provide a function to undo the last move (minimax can make use of this to run without ever making a copy of a Board). A method that returns a hash key of the position is also useful, for implementing transposition tables.

The details of the game are important for picking a board representation, but if it is a game like checkers or chess, with a board that is no larger than 64 useful squares (8x8 checkers uses 32 squares, 10x10 checkers uses 50 squares), you can use bitboards to represent the position. A more straight-forward representation is an array of enums, as already suggested in this thread. Bitboards are better at figuring out things like "what are all the white pawns that can move Northwest" (which takes something like 5 fast assembly instructions). But if you need to check the content of individual squares often, an array is the way to go. Sometimes you want to have both representations available simultaneously: You pay a price in the size of the Board objects (which doesn't matter if you don't copy them around) and in the cost of making moves, but then you can answer both types of questions fast.


If you use a minimax algorithm to search the game tree, you'll need an evaluation function (a heuristic function which given a position returns a number that indicates what the winning chances of each player seem to be). That function will probably need access to the details of how Board is implemented, so you can make it a friend of the class. Sometimes the search function also needs access to details of the Board. When I start writing these programs I am usually not certain about what kind of access is going to be needed, so I make everything in the Board public, which is probably just a bad habit. I am very good at keeping const-correctness in my code, so the usual risks of having everything public are not too bad. Also, if the board representation were to change at some point in the future, I would have to rewrite most of the engine code anyway.

Beyond Board and Move, you can have object classes like Player (an abstract interface which primarily has a method to pick a move on a given position), Engine (derived from Player), GameDataBase, etc. But using classes to model anything below the abstraction level of Board and Move isn't likely to be useful.

An engine typically doesn't need to perform any dynamic memory allocation at all. If you find yourself using `new' all over the place, you are probably doing it wrong. You don't need to keep a game tree structure in memory at all when implementing minimax (if this is not clear to you, I can give you some sample code so you see what I mean).

One more thing: The computer chess community learned over the years that it is extremely useful to define a text-based interface to communicate with an engine. Modern chess programs have separate processes for the GUI and for the engine (or engines), and the standardization allows you to unload an engine, load a new one written by someone else and keep going. The text interface can also be used by a program that arranges matches between engines to gather statistics, which doesn't need graphics at all and can be run in a cluster overnight. This is great for testing changes to an engine, to measure if the change is actually an improvement. Unfortunately there are two competing interfaces: XBoard and UCI (Universal Chess Interface). Neither of them is perfect, but UCI is very reasonable in many respects. For the game of go, there is a dominant interface called GTP (Go Text Protocol).
Advertisement

If everything in OOP had to be a class, we'd be writing code like below, which we don't --
[font=courier new,courier,monospace]Assertion( Comparison( Adder( Integer(1), Integer(1) ).Result(), Integer(2) ).Result() ).Check();[/font]


[source lang="ruby"]smile if Ruby.isBeautiful?[/source]
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

[quote name='Hodgman' timestamp='1353051820' post='5001470']
If everything in OOP had to be a class, we'd be writing code like below, which we don't --
[font=courier new,courier,monospace]Assertion( Comparison( Adder( Integer(1), Integer(1) ).Result(), Integer(2) ).Result() ).Check();[/font]


[source lang="ruby"]smile if Ruby.isBeautiful?[/source]
[/quote]
[source lang="c"]
is_readable? depend_on_dev() : ruby_char_saver();
[/source]
I usually tend to design more in terms of state and transformations.
Think about what structure can represent each valid configuration of your game unambiguously. Then think about which are valid transformations on that data and which actor has the authority to do these.

This should lead to a nice code structure will well defined operations where classes control the integrity of the game state.

EDIT: I realized this sounds pretty vague. Maybe it is because that's the way one should design regardless of language. I guess the TL;DR version would be: Don't think of OOP as a philosophy that forces you to program a certain way. Rather, think of it as a tool set that enables you to more efficiently implement a sensible design.

[quote name='Khatharr' timestamp='1353070319' post='5001521']
[quote name='Hodgman' timestamp='1353051820' post='5001470']
If everything in OOP had to be a class, we'd be writing code like below, which we don't --
[font=courier new,courier,monospace]Assertion( Comparison( Adder( Integer(1), Integer(1) ).Result(), Integer(2) ).Result() ).Check();[/font]


[source lang="ruby"]smile if Ruby.isBeautiful?[/source]
[/quote]
[source lang="c"]
is_readable? depend_on_dev() : ruby_char_saver();
[/source]
[/quote]

"Programmer" (n) - A person so utterly maladjusted that they can take a language as beautiful and expressive as Ruby and write something that looks like a declaration of war composed by a drunken, illiterate Klingon.

Some people's Ruby makes me want to hurt myself.
...
Or them.

lol
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

[quote name='SuperVGA' timestamp='1353079295' post='5001555']
[quote name='Khatharr' timestamp='1353070319' post='5001521']
[quote name='Hodgman' timestamp='1353051820' post='5001470']
If everything in OOP had to be a class, we'd be writing code like below, which we don't --
[font=courier new,courier,monospace]Assertion( Comparison( Adder( Integer(1), Integer(1) ).Result(), Integer(2) ).Result() ).Check();[/font]


[source lang="ruby"]smile if Ruby.isBeautiful?[/source]
[/quote]
[source lang="c"]
is_readable? depend_on_dev() : ruby_char_saver();
[/source]
[/quote]

"Programmer" (n) - A person so utterly maladjusted that they can take a language as beautiful and expressive as Ruby and write something that looks like a declaration of war composed by a drunken, illiterate Klingon.

Some people's Ruby makes me want to hurt myself.
...
Or them.

lol
[/quote]

That's true. Yet have I to see a language that assures whatever is written to look beautiful.
There are war-declaring drunk illiterate klingons everywhere. :D
I do think Hodgmans example involved somewhat more levels of encapsulation, but I do get what you mean.
There are no "right" ways to do OO design, just like there are no right ways to do data modeling. There are ways that let you get working code, with more or less trouble than other ways, for different values of "working code" and "trouble".

But I'm going to illustrate some code suggestions, with incomplete examples, if you'll allow me the liberty. My goal is simply to get you thinking and get your design juices flowing with some concrete starting points.

First, I'm going to assume that I'm dealing with a fixed size 8x8 chess / checkers style board, and a game with well know rules and fixed sets of pieces. Not because that matters, but because it will let me focus on certain parts of the problem first, while changing those things would require going a few levels further down the rabbit hole.
[source lang="plain"]
Player
Name

Piece
Type - { Knight, Rook, etc. }
Owner : Player

Square
Color
HasPiece : bool
Piece

Board
ClearBoard() - makes the board completely empty

AddPiece(Piece) -
RemovePiece(Location) - removed the piece at the specified location
MovePiece(Location source, Location destination) - take a piece off of source, puts on destination
- in my version I raise an error if no piece exists at source, and also if destination is not empty.
- that's because I want my "game engine" deciding if pieces can kill pieces, etc - not my board.

SquareAt(Location) : Square - returns the square requested, or throws out of range exception

EachSqaure() : Enumerator<Square> - enumerates all 64 squares
... you can write other enumerators that might be useful for your game, such as ...
EachSquareInRow(int) : Enumerator<Square> - returns the list of squares in the specified row
EachSquareInColumn(int) : Enumerator<Square> - returns the list of squares in the specified column
EachSquareForPlayer(Player) : Enumerator<Square> - the list of squares with pieces of the provided player

ChessEngine
InitializeChessBoard(Board emptyBoard, Player white, Player black) - constructs and places the white and black pieces on the board.
IsValidMove(Board, Player, Location, Location)
IsInCheck(Board, Player) : bool
IsInCheckmate(Board, Player) : bool
EachValidMove(Board, Player, Location) : Enumerator<Square> - returns each move the player can make from the selected location. returns an empty list if no valid moves exist

ChessGame
Board : Board
Players : Player[2]
Turn : int
Phase : int - 0 for white, 1 for black
[/source]
I don't have time to go any further right now ... and of course this design would change and evolve for any real-world coding exercise ... I just wanted to give you some concrete thoughts about the types of things that might be classes and their methods. Good luck.
The best OOP exercise I can think of with checkers is the difference between a regular piece and king.
Chess would probably offer a lot more exercises due to the differing pieces; now you can have a (virtual) method to select the icon to draw, a (virtual) method to get the valid-moves-template, etc...

In checkers you have to force it by having a IPiece interface and implement it in a RegularPiece and a KingPiece class.


Oh as mentioned above 'moves' are a great OOP exercise - it's a proxy for Undo which is a beautiful OOP example.
You have an IMove interface that has two methods, Move and Unmove.
Then you have two stacks, the Undo stack and the Redo stack.
Whenever you make a move you create a Move instance with all the info you need to make the move then execute the 'Move'.
Then push it onto the Undo stack. If the user waps an Undo, you pop the Undo stack and 'Unmove' then push it onto the Redo stack.
(If they wap Redo, you pop the Redo stack and execute it.)
If the Undo or Redo stack is empty, grey-out the respective buttons.
- The trade-off between price and quality does not exist in Japan. Rather, the idea that high quality brings on cost reduction is widely accepted.-- Tajima & Matsubara
This isn't necessarily directly applicable to your case (right now, at least), but I think thinking about Antiobjects more can be very useful and constructive to good design. For example, I once made a game where there were hundreds of units doing pathfinding on a decently sized map. The "normal" OOP methodology would be to make each unit an Object of some sort and give it the ability to find a path given some information (like a game board of tiles), or make some helper class that, given a unit and a board, finds the path. Or something like that. After reading (part of) that paper, I instead made the tiles of the game board Objects and the tiles did the pathfinding, and the overall interface and design was greatly simplified after doing that.

Anyway, I just want to point out that when considering how do design your objects and relationships, sometimes the "better" solutions require you to think outside the box, where an Object (and its role) isn't the obvious.
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

This topic is closed to new replies.

Advertisement