October 2004 - Posts
Darren keeps asking me why I'm not posting everything on the site, and my major push-back is that it wasn't easy. It still isn't as easy as I would like, but my concentration on some new tools is a start. PDDrop is a red-dot tool that allows you to log-in to PD through the web services interface, obtain a listing of your current projects, and then use the tool as a drop target to place zip files that can then be added as releases. The entire process takes about 15 seconds, maybe a bit more if you are verbose with your notes, and then the release is on the web. Very nice if you ask me. The current release has been posted using the tool, so I'm in the process of a dog food release. If you have an account on PD where you can manage your own projects then you'll definitely find this useful. If you don't, then bug Darren to add you. We only have about 30 registered users and only a few projects.
Project Distributor :: DigiTec Web Consultants :: Project Distributor Client Tools
Actually, I didn't make it to be fun or silly, but rather to create a neat little client-side application for a good friend of mine. Could have just as easily written a basic PRNG index swizzle and gotten identical results, but it seemed like this chance tree is going to just make my life entirely easier if I have it always available as a tool. Not going to worry about implementing the swap filters and other features, but if you need to do some random selection in a web browser, maybe you'll find it useful.
Code-Only: Client-Side OO Javascript Vector Chance Tree for probability selection.
Got asked about a roman numeral parser during an interview. I have to say that I don't mind when the process of obtaining employment plays into my strengths. The process was quite similar to a previous process where I wrote a spoken numerics converter. Not only that, there were many similar qualities to my int parsing routines. With that in mind I think I did fairly well. The goal at the time was to produce a routine to validate numbers up to roman numeral 30 or XXX. Didn't take long, but in the end, I had left out many different validation techniques. I really wanted to revisit the problem since I had the code correctly written in my mind. Check the algorithms out, they should handle just about anything you can throw at them at this point. If you find issues, please feel free to submit your problems, since I'd love to solidfy things a bit more. Apparently roman numeral parsing has great application in reading dates.
Roman Numeral Parsing: Code Only: Bidirectional roman numeral parsing. [EDIT: Added alternate parsing routines and performance fixes]
Integer to Spoken Numerics: Code-Only: int/long/double conversion to Spoken Numerics
Phone Number to Words: Trying my hand at the old Phone number to Words teaser project!
Integer Parsing: DWC.Algorithms.NumberUtilities
The BitBoards so far have been astoundingly accurate at producing moves. But even after the moves have been produced they have to be fully validated. Take for instance, a bishop in the middle of the board. The number of potential moves for the bishop is 13 or so, but the number of valid moves, unless no spots are blocked, is much less. Further performing friendly versus non-friendly extension is extremely importan since you can't move into a friendly position, but you can move into the first occuring non-friendly position (capturing). I've found some interesting transformations here, but once I can more fully validate them I'll start to post their intricacies.
Even more frustrating are the pawns. Pawns are capable of special feats when in their original file (forward by 2), they are allowed capturing moves that are different from their standard movement rules, and they are also allowed the ability to capture en-passant. Deciding where and how to implement these extra conditions is very important. Remember the original blocking square algorithm I implemented for removing invalid moves consisted of:
uint myPieces = ...; uint notMine = ~myPieces; uint validMoves = moves & notMine;
This has to be expanded a bit, since notMine actually points to all empty and enemy squares. What we need now is a blocking region for all enemy squares (which we have to store anyway, since eventually the board swaps sides and enemy and friendly are reversed). The valid moves become something like:
uint nonCapture = pawnNonCapture[x]; uint capture = pawnCapture[x];
uint validNonCapture = (~(myPieces & enemyPieces) & nonCapture);
uint validCapture = capture & enemyPieces;
if ( board.enPassant ) { /* special case when prior moves allow */ }
uint validMoves = validNonCapture | validCapture;
Even with all of this magical bit-shifting we aren't accounting for visibility off the first rank. After all, if we are blocked forward 1 and not forward 2, this might still give that as a valid move. After all of this work on pawn movements, it might be better just to leave them to special cased code because of their intricacies compared to other pieces. What can we do then to quickly generate pawn moves? Some ideas are floating around for sure. If I have a BitBoard of all my pawns I can easily do the following:
uint validMoves = (curPawns << 8) & (~(myPieces & enemyPieces)); // one sided, but basically advance and check.
validMoves |= ((validMoves & 0xFF0000) << 8) & (~(myPieces & enemyPieces)); // all that were able to move forward 1, move 2
Some of the above information would be pregenerated, but you can see all of the gruesome details in action. Captures are a basic extension of the above. The basic premise involves 7 or 9 bit shifting operations along with clearing of the overflow file. What do I mean by overflow file? Well, take the rightmost file, h, and then tell me how a pawn would attack off the right side of the board. If we shift by 9 still the pawn in file h, would wrap around and attack the other side of the board. Clearing the pawns in the oveflow ranks is the only way I can think of getting rid of this for now.
uint validCaptures = ((curPawns & clearRank[0]) << 7) & enemyPieces;
validCaptures |= ((curPawns & clearRank[7]) << 9) & enemyPieces;
It is fairly nice to be able to calculate all pawn moves simultaneously in this manner. I'd still calculate en-passant separately because it has side-effects. The attacking square is different from the square where the enemies piece would be removed. It is an extra piece of information that the board has to carry around to make sure that it always performs the appropriate captures.
If you have questions about some of the rules, I actually found the following a bit helpful. I have a chess rules book that I tend to go to first for explanation, especially since I need to ensure accuracy in the engine. When I don't feel like leafing I'll check something out here in the FAQ and then put a little comment in the code to go back later and check the official rules http://www.chessvariants.org/d.chess/faq.html. Even if you know how to play chess, these FAQs can be algorist playgrounds, since often overlooked newbie questions can point out subtleties, patterns and optimizations you might not otherwise dream-up.
Two reasons for this really. First, I promised I'd do something with formalizing the code originated by Tim when I originally posed the problem. Second, a reader has had some issues downloading the original zip file. Since we have the opportunity we'll increase the original problem to coding any series where we know the range and the number of elements we'll be encoding. If you need to code an arbitrary number of elements in a range then you'd have to add additional support for that.
public static int[] DecodeSeries(int code, int minValue, int maxValue, int length) {
int[] decode = new int[length];
int range = maxValue - minValue;
int lastValue = 0;
for(int i = 0; i < length; i++) {
for(int j = lastValue;;j++) {
int coefficient = BinomialCoefficient(range - j, length - i - 1);
if ( coefficient > code ) {
decode[i] = j + minValue;
lastValue = j + 1;
break;
}
code -= coefficient;
}
}
return decode;
}
public static int EncodeSeries(int[] series, int minValue, int maxValue) {
int encoding = 0;
int range = maxValue - minValue;
int lastValue = 0;
for(int i = 0; i < series.Length; i++) {
for(int j = lastValue; j < series[i] - minValue; j++) {
encoding += BinomialCoefficient(range - j, series.Length - i - 1);
}
lastValue = series[i] - minValue + 1;
}
return encoding;
}
public static int BinomialCoefficient(int n, int k) {
int numerator = 1;
int denominator = 1;
for (int p=0; p<k; p++) {
numerator *= (n-p);
denominator *= (p+1);
}
return numerator/denominator;
}
Sorry, no time for huge explanations, but hopefully the code is quite usable and self-explanatory. I think discovering code through it's output is important and so I'll also give you two test programs that help to verify the output of the methods. The first set of tests handles the poker hands by verifying the uniqueness during the encoding process, that the decoding process works correctly, and shows one way of calling the methods.
int items = 52; int selections = 5;
int[] tenFive = new int[BinomialCoefficient(items, selections)];
for(int i = 0; i < items - 4; i++) {
for(int j = i + 1; j < items - 3; j++) {
for(int k = j + 1; k < items - 2; k++) {
for(int l = k + 1; l < items - 1; l++) {
for(int m = l + 1; m < items; m++) {
int encode = EncodeSeries(new int[] { i, j, k, l, m }, 0, items - 1);
int[] decode = DecodeSeries(encode, 0, items - 1, 5);
tenFive[encode]++;
#if DEBUG
Console.WriteLine("{0}, {1}, {2}, {3}, {4}",
decode[0], decode[1], decode[2], decode[3], decode[4] );
#endif
if ( i != decode[0] ) { Console.WriteLine("Failure"); }
if ( j != decode[1] ) { Console.WriteLine("Failure"); }
if ( k != decode[2] ) { Console.WriteLine("Failure"); }
if ( l != decode[3] ) { Console.WriteLine("Failure"); }
if ( m != decode[4] ) { Console.WriteLine("Failure"); }
}
}
}
}
}
Console.WriteLine("Verifying {0} combinations", tenFive.Length);
for(int i = 0; i < tenFive.Length; i++) {
if ( tenFive[i] != 1 ) {
Console.WriteLine("Failure");
return;
}
}
}
That isn't a very interesting combination though. We've done the poker deck already and it was fun. Other series may be important as well. We can encode a series of numbers in the range of -10 to +10 using a slight variation.
int low = -10; int high = 10;
int selections = 5;
int[] tenFive = new int[BinomialCoefficient(high - low + 1, selections)];
for(int i = low; i < high + 5; i++) {
for(int j = i + 1; j < high + 4; j++) {
for(int k = j + 1; k < high + 3; k++) {
for(int l = k + 1; l < high + 2; l++) {
for(int m = l + 1; m < high + 1; m++) {
int encode = EncodeSeries(new int[] { i, j, k, l, m }, low, high);
int[] decode = DecodeSeries(encode, low, high, 5);
tenFive[encode]++;
if ( i != decode[0] ) { Console.WriteLine("Failure"); }
if ( j != decode[1] ) { Console.WriteLine("Failure"); }
if ( k != decode[2] ) { Console.WriteLine("Failure"); }
if ( l != decode[3] ) { Console.WriteLine("Failure"); }
if ( m != decode[4] ) { Console.WriteLine("Failure"); }
}
}
}
}
}
Console.WriteLine("Verifying {0} combinations", tenFive.Length);
for(int i = 0; i < tenFive.Length; i++) {
if ( tenFive[i] != 1 ) {
Console.WriteLine("Failure");
return;
}
}
Many of the greatest problems are solved by an unknown individual. With this single concept in mind I've worked off and on trying out my skills at the Riemann Hypothesis... Getting nowhere fast, if anywhere at all. Having an understanding of the problem and the skills to investigate the work done by others is fairly rewarding and interesting. The problem actually provides a fairly high bar for those that might be interested in lending a hand. So forget it, been there done that, on to another million dollar problem.
Turns out the P vs NP issue might be a bit easier for the computationally minded. In fact it is warranted as the single most understandable millenium problem for the uninitiated. With that in mind it is also touted as the problem most likely to be solved first. Whether or not that is still the case as most of the research thrives around a couple of the core problems, I'm not sure. They may just fall first by pure brute force. Anyway, I check the Clay Institute website since every now and then you find a good link or two http://www.claymath.org/millennium/P_vs_NP/.
I have never really considered touching the P vs NP problem, but now I think it might be interesting. Seems back in 2000 Richard Kaye published a paper on the NP-completeness of Minesweeper. You can find out about that here http://www.claymath.org/Popular_Lectures/Minesweeper/. Even more fun, I think, is that he devises the proof that Minesweeper is NP complete by demonstrating the SAT problem can be rewritten in terms of Minesweeper diagrams. Some of them are pretty crazy, so you'll have to check them out, especially the AND gate and a number of the crossover diagrams.
Interested in why I'm bringing this up? Well, that travelling salesman problem is also NP complete and that means that optimal solutions to Squirrel .NET are also NP complete and for non arbitrary cases require an NP complete algorithm and can't be completed in P time. If you want a chance at a 1 million dollar prize you should head back a few posts (quite a few actually, I type a lot), and maybe give Squirrel .NET a few tries and then drop me an email to give you some harder versions of the puzzle.
For the Project Distributor application we want to implement some caching. We have a number of options that we can use for this process, including making use of the default ASP .NET caching, but since I had some spare time I figured I'd throw 30 minutes at writing my own cache. I'm looking at a number of techniques based on my performance knowledge and hopefully I've identified most of the common methods of running a cache. The first problem is allowing keyed entries and it can be solved in many ways.
- Use a Hashtable or a NameValueCollection or any other key indexed collection that might suit your fancy.
- Use something like a SortedArray that I've written about previously. These are fast and they work well for small numbers.
- Use a tree or skip-list as noted by some other individuals.
I'll let the cat out of the bag. I've chosen the SortedArray because it really is fast until you reach an insanely large number of items. Because it is sorted, key lookups are a matter of doing a quick BinarySearch. Picking the collection is only half the problem. Next you have to decide how much additional data to store with your items so they can be properly collected. You at least need to store a last modified time, else you'll get into trouble quick. Remember cache items expire after a certain period of time in most cases (and more rarely at the occurence of an event).
If we store just the date, what does a recycling process look like? Well, we have to iterate through the entire collection and look at the dates to see if the items should be collected. This is fairly bogus and would take a large amount of time for us to accomplish. Another option would be to store an ordered list of last access times. Then the problem becomes that during each update we have to find the original item in that list, remove it, and add a new item to the end of the list with the new time. Darn, that hurts as well, but since it is ordered, and new items get appended to the end, the only management will be shrinking during a removal.
If we decide to store more data with the item we can do something else entirely. A version number that gets incremented on each access. You might ask what this accomplishes. Well, now rather than storing a collection of dates that must be managed we can store a queue of access information. Each access results in the item's key and the version number being placed in the queue. Now, during a recycle operation we can check the front of the queue, and then look up the associated item. If the version numbers are different, we continue, otherwise we do the date check. We'll cycle through the queue all the way up to the first matching version number where the date hasn't timed out. This isn't without it's trade-offs though, so I'll enumerate some of them.
- The queue can grow as large as the number of accesses that occur during your cache expiration period.
- Working through the queue requires a BinarySearch for each item. If the same item is being requested many times over it will have many entries into the queue and you'll spend time searching for the same item over and over.
Depending on your back-end those can be pretty hefty problems. What I'll point out here is that for a small queue of frequently accessed items, you won't use an LRU queue at all. You'll instead cycle through the entire queue for recycling. If you don't need items to preemptively remove themselves, you can wait until they are accessed beyond their timeout period and then remove them at that time. There are other structures you can add, such as an MRU queue where you keep the last N used items. Any items not in the MRU queue get removed from the system during a recycle.
How does a recycle work? Well, plenty of heuristic options for that as well.
- Shortest period timeout. This option relies on finding the next item that will be removed and setting a timeout for it. One that item is removed you get the next shortest interval and set a timeout for that as well. Timeouts get allocated into buckets depending on how far in the future they reside to increase lookup performance.
- Access timeouts work based on the user's use of the cache. If the user doesn't access the cache there aren't any timeouts and things sit. The more the user operates on the cache the more often the timeouts occur. This is simple enough using a basic byte. You can increment the byte each access and then recycle on a wrap-around to 0.
- Cache barrier recycling. A cache barrier is when you allow only a specific number of items in the collection before you try to recycle. ASP .NET implements a mixture of cache barrier recycling (with priorities) and shortest period timeout. The benefits of cache barrier recycling are that you can throw out items until you come back under your barrier, irregardless of whether or not they've timed out. The bad thing here is that your application will be forced to fetch data more frequently.
A generic cache of any sort is actually harder to create than you might think. After all, the options that work at low loads don't scale to higher loads. The options at higher loads are very fast when there are lots of objects, but not as fast as a more specialized cache over smaller numbers of objects. In the end, the cache I'll be finalizing for storing objects will rely on a sorted array with binary search capabilities, a customized add method that handles version number consistency (write over top instead of add alongside), a customizable cache timeout (the only way for items to get removed), a linear search recycle method that upgrades to an LRU cache after a threshold, and retrieval timeouts just in case you want to turn cache recycling off.
This is a direct request from a reader related to some of my past Poker engine work, so if you want a .NET post, then catch me later. The primary focus of the question was the design and architecture that is instituded by the larger poker sites in order support multiple thousands of simultaneous users and games while still handling all of the more basic aspects of online gaming like account management, transactional betting, and chat. Rather than start by diving into that, let's first look at the real physical barriers to entering into a multiplayer gaming market with a piece of software.
- The 2 user problem - Getting two users to play is the first hurdle. Here you are tackling the issues of getting your network communications working, really understanding the synchronization of multiple players on the server side, and basically running through the majority of the work to create a successful multiplayer game (assuming you only want to support a couple hundred users).
- The several hundred user problem - There is a barrier, generally at the server level where several hundred users is almost impossible achieve. At this stage you are running into message routing issues, your CPU is getting bogged down, and you are stressing your hardware a bit. This is where you start to realize a couple of things. First, you can still double or triple your base by fixing the server, the message protocol, and performance tuning some more. We've discussed some heinously simple ways to compress cards for instance into 1 network packet along with a bunch of other information. The second thing you realize is that distribution and adding machines is also going to help, but at an expansive management cost and with synchronization problems built in that have to be handled between machines.
- The several thousand user problem - If you are still working on one machine, which is possible, then you've hit a hard limit in socket and network performance. You have to start distributing at this point and/or buying special hardware. At this boundary you are most likely going to offload chat to a separate server, at least general chat, while still performing in-game chat directly through the game channel (game channel communication is fast generally and is why most games support party chat natively, but offload support for global chat to other software and/or servers). You've solved all of the distributed synchronization issues at this point, and you are able to add new servers easily when capacity rises.
- The 50,000 user problem - The holy grail of network gaming, to get to 50,000 users. Very hard with MMPORG style applications, because the server clusters representing a game are really capped at their capacity near 2500-5000 users. That means they've been truly distributing and have at least 10 game worlds. Here you start to get fragmentation of your population and it is hard for people to find those they want to play with. At this point you have dedicated servers for varying levels of play and you've focused on specific servers for tournaments and competition. Solving the user connection problem is going to be difficult, but it will be the primary and possibly only goal at this stage (discounting bandwidth and throughput issues you are having with your ISP and them bitching about the cost of bandwidth going up when in fact the cost has steadily decreased. I love my ISP folks, they actually give me rebates when that happens.).
Lots of problems at every level. No reason not to tackle them though. In fact it isn't all that difficult to manage. Look at the MSN Online Gaming Zone as a great example. They've logically broken their entire site down into the concept of rooms. Once you've gotten into a room you are either sharing a server with some other rooms or you are on your own server. At this level players can logically choose opponents from those available and start a game. Each room has only 50 tables going at once and while these 50 tables probably don't represent a single server playing the game they could. Players are always running on a dedicated server and synchronization issues with the back-end are kept to a minimum. Basic security consistency checks can ensure there aren't dual-logins and that only one game server can update a players account at a time. Failures are easily assigned to particular servers and they can be brought offline and fixed quickly while games are occuring across the rest of the farm.
How do online poker sites solve this problem? They don't! They buy the software pre-canned in almost every circumstance. There are many reasons for this, but the most basic is being licensed to serve gambling content. Ever been to a casino and play the electronic poker games? Those are extensively tested to make sure their odds are consistent and then licensed for use. You are dealing with people's money so there has to be some oversight as to how the money is handled to ensure some level of security. Think of the gambling site like a brokerage, and you the player, are a stock trader. You'd be pretty scared if the bank's software wasn't licensed and checked to make sure your money couldn't leak elsewhere. You'd be even more frosty if your shares and/or payouts weren't properly reassigned back to your account because of some glitch in the system or maybe even a server crash.
Obviously the original sites produced some of their own software, but a burgeoning market is selling full blown poker engines that are already licensed and ready to go. The company gets a support license, and after the appropriate amount of marketing has hopefully reached the critical mass and is making money to move forward. Gambling is one of the few institutions where the site can actually start breaking even in a rather short time period, especially for sites that are not bent on massive growth, but instead supporting a steady and fixed population of users. After all, 3% of a $50k table is going to yield you just as much as 3% of several hundred casual gambling tables. Gives you a market to cater to if you just want to put a small server out there and still make decent cash by playing to those that want special services.
To wrap-up I'll give some architecture design guidelines that should help to overcome many of the boundaries. Many of these are common sense for the hardware gurus or those familiar with distributed processing, but you'd be surprised how easy they are to overlook when you are designing your system.
- Have a connection management plan from the beginning. There are hard limits on connections and you have to either make sure you can add servers or support more connections through better hardware (probably expensive hardware). More servers is the easier option so design for that direction. If you can write a disconnected protocol and the overhead of reconnecting doesn't swamp you, then plan for that.
- Seperate core services and extended services at the connection level. It should be easy to run lobby services (extended) tournament management services (extended) account management services (required but extended) and any others on their own hardware. Core services support the game and that means everything that has to go over the wire during play. Chat is an example of a core/extended service since you can easily add it to your table services, but at the same time you may get better options from offloading it to specialized chat services. Core services also use guaranteed protocols and require you build in lots of fail-safes and contingencies, whereas extended services (yeah, I know, the account manager) don't have to be well thought out. Chat can be run over UDP and doesn't need to be guaranteed and many other services can be run on generic web servers.
- Logically break down the game process into modules. This is the most important for distributed play. You have to ensure that you can easily shell off a new table to a server that is capable of handling it. Remember the 2 user problem? Well during that problem you created a consistent engine. That engine could logically run anywhere, but most likely it ran within the context of a server. Either one of the users was the server or a specialized server existed somewhere else that they both connected to. The engine hosted by the server then, is your module for distribution. Note the game servers themselves will still host services, such as authentication, connection management, message routing, and account interaction.
Just those three considerations will get you well along the way. There are still many problems to solve, but hey, if you are making the big dollars and want me to give you some more hints, then share the wealth and we'll talk ;-) These problems used to be cutting edge research, but they are now well known domains with many interesting solutions. Most solutions at the higher marks are still very customized and rather than maintain the distributed model, many implementations try to squeeze the extra performance out of what they have. I don't have a problem with that, but it does kill the reusability of their solution just so they can support an extra few thousand users that could have been supported with an extra server. In many cases they don't have proper load balancing and didn't isolate the game modules for distribution. In others they have the problem that load balancing can be overcome by the user (almost every MMPORG has no say if every user shows up in the same zone). New games have overcome these issues (City of Heroes) with ingenious solutions that we either love or hate. Hopefully we'll see more of that modular approach with a focus on reusable and portable solutions.
Yeah, yeah, yeah, I'm still pimping out Project Distributor, but it really is a great little tool. We should be able to go live and take new users/groups soon, and the source code may get posted up as a project as well so you can host your very own toolbox. I'll talk about some of the cool features that we'll be adding at a later time though, now is the time for Chess!
Well, the new tool is basically a source dump of my previous engine and some functions from the new engine. I've also added a specialized app that lets you see the move generation in action. You can click on board locations, select the piece type and you'll see your starting location as a black dot and all movement locations as a series of red dots. Not very cool, but it gives you an idea of what is going on. If you find some cool ways to generate moves quickly that I haven't hit upon, plug them in, see if they work by using the visualization UI and then let me know. Maybe we'll suck that bad boy into my final engine.
Project Distributor::DigiTec Web Consultants::Chess Tools::0.1 Alpha

There are basically two interesting ways to create rook moves. The first is what I call striping, and it involves creating data points for each rank and file on the board. This requires you create 16 longs and can be done very quickly. In fact creating the first rank manually, and then bit-shifting it produces the remaining 7. The same happens with each of the files. Once you've completed this you combine the bits from the rank and file arrays for each spot on the board using integer division and modulus arithmetic on the location offset.
That is one way to do it, or you can generate the rank/file information on the fly for each piece. This is where the bit shifting algorithm comes into play and I'll examine it inline.
private static ulong[] GenerateRookMoves() {
ulong[] moves = new ulong[64];
ulong bitMask, bit1, bit2;
for(int i = 0; i < 64; i++) {
bitMask = 0;
We are looping 64 times for all possible spots. Every spot on the board produces a unique set of move combinations so we can't take any shortcuts. The first bit we create is the rank bit. The rank bit is generated by shiftin a single bit left by 0 to 7 spots. We need the first spot in the rank so we can iterate through all 8 ranks from start to finish. The second bit or file is is generated by getting the first spot in a given file using division and multiplication by 8. Here again, we'll select a bit that is a multiple of 8 starting with 0 and going up to 56.
bit1 = ((ulong) 1) << (i&7);
bit2 = ((ulong) 1) << ((i>>3)<<3);
for(int j = 0; j < 8; j++) {
bitMask |= bit1 | bit2;
Each of the bits are bitmasked together as we iterate the rank and file. Each rook can perform 15 different moves (14 really, but we'll clip that 15th with the piece blocking bits). Finally two shift operations select the next bits. The rank bit is shifted by 8 to move rank, and the file bit is shifted left by one to move up the file. Seems pretty obvious when you see it.
bit1 <<= 8;
bit2 <<= 1;
}
moves[i] = bitMask;
}
return moves;
}
Remember, we don't need the maximum amount of speed when generating these arrays because we'll pregenerate them and continuously use them. Certain small devices might be an issue, where you need to generate these on the fly, but remember you can always get just the value you need by constraining the outer loop. Using the first method mentioned with the 16 pre-generated values may also be a great option on a smaller device because you can more quickly get generate all 64 values with only a quarter of the storage. The loop after all, isn't free, even though you are only doing a couple of shift operations. While I'm thinking about it, I should start working on a visualization tool for these things so you can more easily examine the results. Verification is super important and I'm not sure the huge 64 line spit-out showing moves from a given position is the best way to go ;-)
More Posts
Next page »