• Welcome to the new COTI server. We've moved the Citizens to a new server. Please let us know in the COTI Website issue forum if you find any problems.
  • We, the systems administration staff, apologize for this unexpected outage of the boards. We have resolved the root cause of the problem and there should be no further disruptions.

Calculating Hexes within Jump Range

RKFM

SOC-11
Does anyone have an algorithm that can calculate the reachable hex coordinates based on origin hex coord and jump range? I've done a lot of Googling and searched this forum and the best I could find is this: http://www.travellerrpg.com/CotI/Discuss/showpost.php?p=192456&postcount=84

However, I'm not familiar with perl or PHP and can't really see how to adapt this to VB or C#.

Can someone outline the logic behind this or provide some example coded in Vb or C#?

Much appreciated,

RK
 
Shouldn't be too hard. I can do VBScript, not C.

Where are you getting your data from ? (The Traveller Map I hope.)

http://www.travellermap.com/sector.htm?sector=Spinward+Marches

That has both the planetary data, plus the hex map.

>

I can understand VBScript, thanks.

To be more specific, I'm looking to find all reachable hexes (based on sector hex coordinates i.e. 0101 to 3240) from an origin hex depending on the jump range. For example, what hexes can be reached with a jump 4 ship from say, Regina? I don't need to know the specific systems, just all possible hexes. A generic function, one that maybe returns an array of all reachable hexes.
 
somewhere I found calculating the distance between 2 hexes (C# below). But now I cannot remember where. Unfortunately this only gives you the ability to find the distance, not figure all within a range. But some reverse engineering....

Code:
        /// <summary>
        /// calculate the distance between two hexes
        /// </summary>
        /// <param name="W1">first hex number; 1234</param>
        /// <param name="W2">second hex number; 5678</param>
        /// <returns>int - distance in jumps/parsecs</returns>
        public int calcDistance(string W1, string W2)
        {
            int ST1 = Convert.ToInt32(W1.Substring(0, 2));
            int CR1 = Convert.ToInt32(W1.Substring(2, 2));
            int ST2 = Convert.ToInt32(W2.Substring(0, 2));
            int CR2 = Convert.ToInt32(W2.Substring(2, 2));
            int STDistance, CRMin, CRMax, CRMod;

            STDistance = Math.Abs(ST1 - ST2);

            int CROffset = (STDistance / 2);

            int mod1 = ST1 % 2;
            int mod2 = ST2 % 2;
            if (mod1 == 1 & mod2 == 0)
                CROffset++;

            CRMin = CR1 - CROffset;
            CRMax = CRMin + STDistance;

            CRMod = 0;
            if (CR2 < CRMin)
                CRMod = CRMin - CR2;
            if (CR2 > CRMax)
                CRMod = CR2 - CRMax;

            STDistance += CRMod;

            return STDistance;
        }
 
also, scroll down this page a bit for how to do this in Perl for the neighboring hexes (possibly it could be extrapolated for rows out): http://taskboy.com/?tag=games

This page seems interesting but perhaps a tad over my head at the moment : http://www.sable.mcgill.ca/~clump/hexes.txt (html format here oddly: http://www-cs-students.stanford.edu/~amitp/Articles/HexLOS.html)

and another method (which would be interesting to work out - maybe I'll play with that) would be to take the initial hex, and for each range (0 = initial hex only, 1=adjacent, 2=2 hexes away) a sort of recursive routine cycling around the hexes. Suppose our hex is 0405. Range 0 =0405. Range 1 = range 0 + adjacent(0405) where adjacent(0405) would break down the hex into column/row (04/05) and calculate the hexes adjacent to that (0404, 0505, 0506, 0406, 0306, 0305). Range 2 = range(0) + adjacent(0405) + adjacent for each of those adjacent hexes (hence the recursive nature). But that would yield a lot of duplicates to purge out, even if the code may be relatively simple.

I think it can be worked out mathematically - obviously we can get the ones straight up and down simply by adding/subtracting the range (0405, the 05 being the row height, so we know for a 4 range, 0401..0409 is valid). It is just those pesky hexes on the sides...

Let me think through this a bit more and stare at a hex map...I know other people have done this and probably have a good solution.
 
Well, each jump-1 adds 6*JN onto the previous total so,

Jump-1 means 6 hexes are reachable
Jump-2 means 6+12 hexes are reachable
Jump-3 means 6+12+18 hexes are reachable

and so on...

So you just need to find the "column" adjust it by + or - the Jump #
and then use the hex # and increment or subtract that by the same amount.

?

Regina = 1910
Jump-1 means columns 18, 19 and 20 are reachable and then:

18-09
18-10

19-09
19-11

20-09
20-10

are reachable hexes.

Jump-2 means all the J-1 hexes, plus columns 17, 21 and there should be
2 hexes from own-row, plus 5x each new row, I think.

I suppose with micro-jumps you'd have to include the original hex #.

>
 
I've got one I use that's in Perl. It calculates all of the hexes at radius n from a given hex. I could translate it to JScript, but vbscript is probably out because I used some list magic. C# with collections would be overkill.


The algorithm I used is:

Know how to calculate all six adjacent hexes. This is different for odd and even x values.

1. Set the hexes for radius 0 to the specified hex.

2. Set the list of previously seen hexes to the specified hex.

3. For each radius from 1 to the requested radius:

a. For each hex in the previous radius find hexes 1 hex away

b. If the hex hasn't been seen, add it to the list of hexes for this radius and add it to the list of previously seen hexes.

I found it easier to generate all of the hexes of a given radius than running a distance formula.

The code in a spoiler block to keep the page short. If you aren't comfortable with Perl, this may make your brain hurt:

Spoiler:

Code:
#!/usr/bin/perl -w

=head1 radius.pl

=head2 Usage:

radius.pl hex radius

=head2 Description:

Provides a list of hexes of each radius from hex of given radii.
This is performed by finding all hexes one hex away from the current 
radius and removing any duplicates or hexes already in a lower radius.

=head2 Method:

1. Set the hexes for radius 0 to the specified hex.

2. Set the list of previously seen hexes to the specified hex.

3. For each radius from 1 to the requested radius: 

a. For each hex in previous find hexes 1 hex away

b. If the hex is new, add it to the list of hexes for this radius and add it to the list of previously seen hexes.

=cut

use strict;
use warnings;
use utf8;
use Carp;

package main;

=head2 Functions:

formathex: shorthand to expand (x,y) to a 4 digit number xxyy

=cut

sub formathex {
    my ( $x, $y ) = @_;
    return sprintf( '%02d', $x ) . sprintf( '%02d', $y );
}

# *****************************************************************
# Read command line arguements
# *****************************************************************
if ( @ARGV < 2 ) { Carp::croak "Usage: radius.pl hex radius"; }

my $hex    = shift @ARGV;
my $radius = shift @ARGV;

# *****************************************************************
# Main
# *****************************************************************
my @hexes = ();
$hexes[0] = [$hex];
my %previous = ( $hex => 1 );

for ( 1 .. $radius ) {
    $hexes[$_] = [];
    foreach my $currenthex ( @{ $hexes[ $_ - 1 ] } ) {
        my ( $x, $y ) = ( 0, 0 );
        if ( length $currenthex == 4 ) {
            $x = substr( $currenthex, 0, 2 );
            $y = substr( $currenthex, 2, 2 );
        }
        else {
            ( $x, $y ) = ( $currenthex =~ (/(\d+)\D(\d+)/) );
        }
        my @evenoffsets =
          ( [ 0, -1 ], [ 0, 1 ], [ -1, 0 ], [ 1, 0 ], [ 1, 1 ], [ -1, 1 ], );
        my @oddoffsets =
          ( [ 0, -1 ], [ 0, 1 ], [ -1, -1 ], [ 1, -1 ], [ 1, 0 ], [ -1, 0 ], );
        my @offsets = ();
        if ( $x % 2 ) {
            @offsets = @oddoffsets;
        }
        else {
            @offsets = @evenoffsets;
        }
        foreach my $offsetpair (@offsets) {
            my ( $xoff, $yoff ) = @{$offsetpair};
            my $newhex = formathex( $x + $xoff, $y + $yoff );
            if ( !exists( $previous{$newhex} ) ) {
                push @{ $hexes[$_] }, $newhex;
                $previous{$newhex} = 1;
            }
        }
        @{ $hexes[$_] } = sort @{ $hexes[$_] };
    }
}

# Print the tables of hexes and radii
for ( 0 .. $radius ) {
    print "Radius: $_: Count: ", $#{ $hexes[$_] } + 1, ": @{$hexes[$_]}\n";
}
print "Radius $radius contains:\n";
my @sorted = sort keys %previous;
print "@sorted\n";
 
also, scroll down this page a bit for how to do this in Perl for the neighboring hexes (possibly it could be extrapolated for rows out): http://taskboy.com/?tag=games

This page seems interesting but perhaps a tad over my head at the moment : http://www.sable.mcgill.ca/~clump/hexes.txt (html format here oddly: http://www-cs-students.stanford.edu/~amitp/Articles/HexLOS.html)

and another method (which would be interesting to work out - maybe I'll play with that) would be to take the initial hex, and for each range (0 = initial hex only, 1=adjacent, 2=2 hexes away) a sort of recursive routine cycling around the hexes. Suppose our hex is 0405. Range 0 =0405. Range 1 = range 0 + adjacent(0405) where adjacent(0405) would break down the hex into column/row (04/05) and calculate the hexes adjacent to that (0404, 0505, 0506, 0406, 0306, 0305). Range 2 = range(0) + adjacent(0405) + adjacent for each of those adjacent hexes (hence the recursive nature). But that would yield a lot of duplicates to purge out, even if the code may be relatively simple.

I think it can be worked out mathematically - obviously we can get the ones straight up and down simply by adding/subtracting the range (0405, the 05 being the row height, so we know for a 4 range, 0401..0409 is valid). It is just those pesky hexes on the sides...

Let me think through this a bit more and stare at a hex map...I know other people have done this and probably have a good solution.

Yes, I've looked at all those pages but my math skills are pretty awful. I have also been staring at a subsector map for some time, but just can't see an easy way to do this.

I know that the creator of H&E had this feature, and I've emailed the Traveller Map creator to see how he does it for his API. I could probably come up with some crude method that iterates through all the columns (X axis) based on a hundred IF...THEN statements, but I was hoping someone knows an elegant algorithm or function that only takes a few params.

Deniable,
Ouch, yes PERL makes my eyes glaze over :-)
If you see the link in my original post, there is a solution that I think is in PERL but I can't make heads-or-tails of it. Maybe you could translate it, or outline the logic behind it?
 
Last edited:
I have also been staring at a subsector map for some time, but just can't see an easy way to do this.

What I did (so far) was take an empty map and then use the center square
for starting point.

Then place a "1" in every jump-1 hex. Note their hex#
Then place a "2" in every jump-2 hex. Note their hex#

and so on.

Then basically it becomes a matter of determining which columns
the jump can reach and how far from the original row, which hexes
can be reached.

There's probably a more scientific method. ;)

Then once you compile a list of COLUMN + ROW pairs you could
query a database and return the matches ( and leave out the blank
hexes).

>
 
Code, written in C:

Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void calcHexes(char * list, int max, char * W1, int R1);
int calcDistance(char * W1, char * W2);

int main(int argc, char *argv[])
{
  char *retHex;
  int retSize, range;
  if (argc<2) {
    printf("Usage: %s hex range\n");
    exit(1);
  }
  range=atoi(argv[2]);
  /* should be enough for requested jump */
  retSize=(2*range+1)*(2*range+1)*5;
  if ((retHex=malloc(retSize))==NULL) {
    printf("Not enough memory\n");
  }
  calcHexes(retHex,retSize,argv[1],range);
  printf("%s\n",retHex);
}

void calcHexes(char * list, int max, char * W1, int R1)
{
  int ST1 = atoi(W1) / 100 - R1;
  int CR1 = atoi(W1) % 100 - R1;
  int ST2 = atoi(W1) / 100 + R1;
  int CR2 = atoi(W1) % 100 + R1;
  /* prevent negative numbers or zero */
  ST1=(ST1<1?1:ST1); CR1=(CR1<1?1:CR1);
  /* prevent more than 2 digits */
  ST2=(ST2>99?99:ST2); CR2=(CR2>99?99:CR2);
  char W2[5];
  strcpy(list, "");
  int STC, CRC, prev=0;
  for (STC=ST1; STC<=ST2; STC++) {
    for (CRC=CR1; CRC<=CR2; CRC++) {
      sprintf(W2,"%04d",STC*100+CRC);
      if (calcDistance(W1,W2)<=R1) {
        if (prev+5>max) return; /* no more room for hexes */
        if (prev) strcat(list,",");
        strcat(list, W2);
        prev += 5;
      }
    }
  }
}

int calcDistance(char * W1, char * W2)
{
  int ST1 = atoi(W1) / 100;
  int CR1 = atoi(W1) % 100;
  int ST2 = atoi(W2) / 100;
  int CR2 = atoi(W2) % 100;
  int STDistance, CRMin, CRMax, CRMod;

  STDistance = abs(ST1 - ST2);

  int CROffset = (STDistance / 2);

  int mod1 = ST1 % 2;
  int mod2 = ST2 % 2;
  if (mod1 == 1 & mod2 == 0)
    CROffset++;

  CRMin = CR1 - CROffset;
  CRMax = CRMin + STDistance;

  CRMod = 0;
  if (CR2 < CRMin)
    CRMod = CRMin - CR2;
  if (CR2 > CRMax)
    CRMod = CR2 - CRMax;

  STDistance += CRMod;

  return STDistance;
}
 
mmbutter - cool! at first I thought you were checking the ENTIRE range of hexes (0000 -> 9999), then traced through a bit better & figured out you were only checking the max left/right & top/bottom ranges & comparing them to the range (i.e., hex 0505, for a J5, checks only 0000->1010. It's been a decade+ since I've written in C except for a couple of minor Unix things. I miss that language!) And that distance calculator looks familiar... :)

I think there may be a way to just generate the list w/out having to check all within the max distance, but this will work for the original poster.

I still think a recursive function may work, but that's the CS geek in me I believe: recursive functions are cool but not real applicable in real world stuff due to the potential for massive overhead depending on how many times it calls itself.
 
mmbutter - cool! at first I thought you were checking the ENTIRE range of hexes (0000 -> 9999), then traced through a bit better & figured out you were only checking the max left/right & top/bottom ranges & comparing them to the range (i.e., hex 0505, for a J5, checks only 0000->1010. It's been a decade+ since I've written in C except for a couple of minor Unix things. I miss that language!) And that distance calculator looks familiar... :)

I think there may be a way to just generate the list w/out having to check all within the max distance, but this will work for the original poster.

I still think a recursive function may work, but that's the CS geek in me I believe: recursive functions are cool but not real applicable in real world stuff due to the potential for massive overhead depending on how many times it calls itself.

This is the same way that Josh Bell (The Traveller Map) does his JumpMap API (Except I think he said he just does a bounding box of possible hexes). Seems like this is a common solution, and not a bad one at that. I was hoping there was some simple, clean algorithm :-(

Thanks for the help folks!
 
This is the same way that Josh Bell (The Traveller Map) does his JumpMap API (Except I think he said he just does a bounding box of possible hexes). Seems like this is a common solution, and not a bad one at that. I was hoping there was some simple, clean algorithm :-(

Thanks for the help folks!

yep - and I think Josh Bell broke out how he handles the SEC files using regular expressions. I need that for a program I've been toodling around with. The SEC file I was using (from the HIWG group) does not match exactly the ones on his site, so apparently there are variations on what is supposed to be a fixed-width format (and as an enterprise developer in RL - I really hate that!). He did something with regular expressions to extract out the data, so I need to see if I can borrow that code (fortunately regex is universal and not a Perl or other scripting language-specific thing!)

And thanks mmbutter - I believe I'll save off that code & convert to C# just in case I ever finish one of my Traveller programs. It may be handy!
 
I was hoping there was some simple, clean algorithm :-(

Yeah, me too. I'm currently between contracts (anybody need a software engineer?) so I spent a couple days trying to figure out an algorithm. Because of the "shifting" of the numbering, I don't think there can be one. The cleanest solution that I found was to use a bounding box, then eliminate the corners.
 
yep - and I think Josh Bell broke out how he handles the SEC files using regular expressions. I need that for a program I've been toodling around with. .... He did something with regular expressions to extract out the data, so I need to see if I can borrow that code

You can take a look at robject's UWP Perl module in CPAN (http://search.cpan.org/~rje/Games-Traveller-UWP-0.94/lib/Games/Traveller/UWP.pm)

Here's the actual code my site uses:

Code:
        private static readonly Regex worldRegex = new Regex( @"^" +
            @"( \s*             (?<name>        [^\s.](.*?[^\+\s.])?  ) )? \+?\.* " +    // Name
            @"( \s*             (?<hex>         \d\d\d\d              ) )      " +    // Hex
            @"( \s+             (?<uwp>         \w{7}-\w              ) )      " +    // UWP (Universal World Profile)
            @"( \s+             (?<base>        \w | \*               ) )?     " +    // Base
            @"( \s{1,3}         (?<codes>       .{10,}?               ) )      " +    // Codes
            @"( \s+             (?<zone>        \w                    ) )?     " +    // Zone
            @"( \s+             (?<pbg>         \d[0-9A-F][0-9A-F]    ) )      " +    // PGB (Population multiplier, Belts, Gas giants)
            @"( \s+  (\w\w\/)?  (?<allegiance>  (\w\w\b|\w-|--)       ) )      " +    // Allegiance
            @"( \s*             (?<stellar>     .*                    ) )      "        // Stellar data (etc)
            , RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace );

This is in C#. @"..." is a C#-ism for "ignore backslashes in the string" which makes regexes easier to read/write. (?<name> ... ) is used for named captures. Otherwise it's a fairly straightforward regex.

I have had to make edits to a handful of the .SEC files my site consumes, since whitespace before/after the PBG becomes necessary. You also have to watch out for base codes and travel zone codes being treated as trade codes - hence the minimum field widths.
 
You can take a look at robject's UWP Perl module in CPAN (http://search.cpan.org/~rje/Games-Traveller-UWP-0.94/lib/Games/Traveller/UWP.pm)

Here's the actual code my site uses:
......
I have had to make edits to a handful of the .SEC files my site consumes, since whitespace before/after the PBG becomes necessary. You also have to watch out for base codes and travel zone codes being treated as trade codes - hence the minimum field widths.

many thanks! copied. I'm pretty much just a C# programmer right now (except for some PL/B aka Databus - maintaining the SAME software for more than 20 years! It's been through a couple of different OS, and I had to convert the old COBOL to PL/B a few years back on our first iteration to Unix, but still, 20 years is too long a time to be maintaining the same software I think, but at least I really, really understand it!). Anyway, my LBB2 cargo program can use SEC files and it bummed me out to find that a fixed width file is not consistent across files, and then I recalled this discussion about regex & the SEC files from a year or more ago. Now if only this site had a better search function.

again, thanks. Maybe I'll actually finish a Traveller programming project I've started...:)
 
Back
Top