Developing hex map concept

Some months ago I released some tutorials about hexagonal tiles (and I developed an Halloween game based upon Hex maps creation and rollover and Finding adjacent cells in an hex map 1 and 2).

Later, Douglas Huskins told me 10 years ago he wrote a collection of functions that would help a person navigate hex maps. The functions included: Identifying the adjacent hexes, identifying the shortest path between two hexes, the relative position of a hex from the perspective of another hex and the distance between two hexes.

This can be really useful to make some strategy games.

Unfortunately most of the work seems to be lost, but Douglas still wants to share the concept with us, so he is rewriting it in Javascript.

Read what he says:

« I could not find my electronic copy of the source files.

All I have are some old Visual Basic printouts that lacked much documentation. I have recoded it into Javascript and need to test it.

However, here are two of the functions: Move and Range.

The hex map layout this code works for is based on a hex map with the points laying horizontally. The top and bottom of a hex are flat.

Hexes are numbered such that the first set of digits represent the column number and the second half of the hex number’s digits represent the row number.

A typical hex number would be 0132. That would be a hex in the leftmost column and would be the 32nd hex down from the top.

This type of map comes in two flavors. The way to identify the two types of layouts is to compare 0101 with 0201. If 0101 is above 0201, then it is referred to as “Odd Column Up”.

The code is able to support either flavor. There is a variable set at the top of the file (mapOddUp) which will let the code switch between the two layouts.

The unit (ship/car/etc) that sits in a hex will face one of the hex sides. The faces are numbered clockwise using either 0-5 or 1-6. Again, the code supports both numbering systems by treating 0 and 6 as the same direction (straight up). There is a variable at the top of the file (mapIsUp0) that determines which facing number system to use.

The code has the ability to change the map size. There are max and min values that can be set.

If you have any questions, please let me know. I will convert the remaining functions next week. After that, I will properly test everything and add a simple interface for it. In the meantime, this should help you create hex based games. »

Here it is the source code:

/*
HEX MAP LOGIC
This code was developed and written by Douglas Huskins.

Permission is granted to anyone to use in any program, provided this comment block remains intact
as is AND credit is given to Douglas Huskins for the hex map logic.  Additionally, the person must 
send Douglas Huskins an email stating how and in what it will be used in.  The email address is a dot com
domain named "huskins".  The username (before the at symbol) is "doug".  If you cannot figure out how to
email Douglas Huskins, then a voice mail message at 925-957-0109 will work as well.
*/

// Valid move orders are L(eft), R(ight), F(orward), B(ack), W(ait)
// If this needs to be added to, MoveStep() needs the new orders as well.
var AcceptableOrders = "LRFBW";

// Configure the size and layout of the hex map.
var mapOddUp = true; // If hex 0101 is above hex 0201, then set to true, else set to false.
var mapIsUp0 = false; // Is the "up" facing hex side zero or six? If zero, then true. If 6 then false.
var mapMinCol = 1;
var mapMaxCol = 99;
var mapMinRow = 1;
var mapMaxRow = 99;

// Create an array to store the Row, Column, Facing of each unit.
// The first element is the unit number.  The second element is the hex information.
// Unit zero is reserved for the functions.  The first game piece is stored in UnitArray[1][x].
var UnitArray = new array(21, 5);   // Max of 20 units (ships, cars, whatever the game is about)
                                    // The second attribute is broken down as follows.
                                    // 0: Hex Column
                                    // 1: Hex Row
                                    // 2: Hex Facing
                                    // 3: Unit Type (not needed in this code)
                                    // 4: Unit Status (not needed in this code)


function DisplayHex(UnitNumber,booWithFace) 
{
    // Use this to print a string version of the hex number (with leading zeros, if needed.)
    // If booWithFace is true, then include the facing after a colon.  Otherwise show only the hex number.
    var HexCol = UnitArray[UnitNumber][0] + "";
    HexCol = "0000000000" + HexCol;
    var HexRow = UnitArray[UnitNumber][1] + "";
    HexRow = "0000000000" + HexRow;
    var LabelLength;
    if (mapMaxRow > mapMaxCol)
    {
        LabelLength = mapMaxRow + "";
    }
    else
    {
        LabelLength = mapMaxCol + "";
    }
    var HexFace = UnitArray[UnitNumber][2] + "";
    HexFace = ":" + HexFace;
    if (booWithFace == false)
    {
        HexFace = "";
    }
    return HexCol.slice(-1 * LabelLength.length) + HexRow.slice(-1 * LabelLength.length) + HexFace;
}

function MoveUnit(UnitNumber,MoveOrders)
{
    // This function is the main entry for moving a unit.
    if (MoveOrders.length > 0) 
    {
        // First off, were the MoveOrders properly written?
        // The function CheckAllOrders() will determine if the orders, as a whole are acceptable.
        // Perhaps the game does not allow two turns back to back.
        var booValidOrders = CheckAllOrders(MoveOrders);

        var i = 0;
        var NextMove;

        while (i < MoveOrders.length && booValidOrders) 
        {
            // Parse the next move from the orders and execute it.
            NextMove = MoveOrders.toUpper().charAt(i);
            if (NextMove.IndexOf(AcceptableOrders) >= 0) 
            {
                // Make the move and return the results in Unit[0][x]
                MoveStep(UnitNumber, NextMove);
            }

            // This function call CheckValidMove() will test if the specific orders were a valid move.
            // The function is currently empty and will return a valid move reply.
            // If the map needs to limit access between two hexes, then the function can be modified.
            
            
            // If the move is valid and the unit did not leave the map then process it.            
            if (CheckValidMove(UnitNumber) && UnitArray[0][0] >= mapMinCol && UnitArray[0][0] <= mapMaxCol && UnitArray[0][1] >= mapMinRow && UnitArray[0][1] <= mapMaxRow)
            {
                UnitArray[UnitNumber][0] = UnitArray[0][0];
                UnitArray[UnitNumber][1] = UnitArray[0][1];
                UnitArray[UnitNumber][2] = UnitArray[0][2];
            }
            else 
            {
                booValidOrders = false;
            }
            // Increment and repeat for next orders.
            i++;
        }
    }
}

function FindRange(Unit1, Unit2)
{
    // This function returns the number of hexes between the two units.
    // The return value is simply the number of hexes (not the number of orders it takes).

    // Are the two hexes above or below each other?
    if (UnitArray[Unit1][0] == UnitArray[Unit2][0])
    {
        return Math.abs(UnitArray[Unit1][1] - UnitArray[Unit2][1]);
    }
    
    // Are the two hexes on the same line (same Row)
    if (UnitArray[Unit1][1] == UnitArray[Unit2][1])
    {
        return Math.abs(UnitArray[Unit1][0] - UnitArray[Unit2][0]);
    }

    // Okay, then find the deltas for row and column.
    var DeltaCol = Math.abs(UnitArray[Unit1][0] - UnitArray[Unit2][0]);
    var DeltaRow = Math.abs(UnitArray[Unit1][1] - UnitArray[Unit2][1]) * 2;

    // So, are the two hexes adjacent?  Just need to look at the two unchecked hexes.
    if (DeltaCol == 1)
    {
        if (mapOddUp && IsColEven(HexCol) && UnitArray[Unit1][1] + 1 == UnitArray[Unit2][1])
        {
            return 1;
        }
        if (mapOddUp && IsColEven(HexCol) == false && UnitArray[Unit1][1] - 1 == UnitArray[Unit2][1])
        {
            return 1;
        }
        if (mapOddUp == false && IsColEven(HexCol) && UnitArray[Unit1][1] - 1 == UnitArray[Unit2][1])
        {
            return 1;
        }
        if (mapOddUp==false && IsColEven(HexCol)==false && UnitArray[Unit1][1] + 1 == UnitArray[Unit2][1])
        {
            return 1;
        }
    }

    // At this point, we know the two units are at least two hexes away from each other.
    // To simplify, we will assign LeftHex and RightHex to the values of the Unit Numbers.
    // This way, all math is done from the leftmost hex.
    var LeftHex;
    var RightHex;
    if (UnitArray[Unit1][0] < UnitArray[Unit2][0])
    {
        LeftHex = Unit1;
        RightHex = Unit2;
    }
    else
    {
        LeftHex = Unit2;
        RightHex = Unit1;
    }

    // The next step is to figure out if it is easier to go across then vertical or the other way around.
    // To be more clear:  We will calculate the distance along the spine of the hexes.
    // The question is when we get to the point we leave the spine, do we go vertically or horizontally.
    if (UnitArray[LeftHex][1] > UnitArray[RightHex][1])
    {
        // RightHex is above and to the right of LeftHex.
        // For every two hexes travelled, it will add 2 to the column value and subtract 1 from the row.
        // Next question is: Is the RightHex above the heading 1 spine?
        if (DeltaCol < DeltaRow)
        {
            // RightHex is above the heading 1 spine.  The distance is calculated by figuring out the
            // row value after moving along heading 1 until the columns match.
            if (mapOddUp)
            {
                if (IsColEven(UnitArray[LeftHex][1]))
                {
                    // In this case we will safely undershoot.
                    return DeltaCol + (Math.floor(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
                else
                {
                    // In this case, we may overshoot by one hex if we don't adjust for it.
                    return DeltaCol + (Math.round(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
            }
            else
            {
                // Same as above, however with mapOddUp set to false.
                if (IsColEven(UnitArray[LeftHex][1]))
                {
                    return DeltaCol + (Math.round(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
                else
                {
                    return DeltaCol + (Math.floor(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
            }
            
        }
        else
        {
            // RightHex is on or below the heading 1 spine but above the midline.  Distance is calculated by
            // figuring out the column value after moving along heading 1 until the rows match.
            return DeltaRow + (UnitArray[RightHex][0] - (UnitArray[LeftHex][0] + DeltaRow));
        }
    }
    else
    {
        // RightHex is below and to the right of LeftHex.
        // For every two hexes travelled, it will add 2 to the column value and add 1 to the row.
        // Next question is: Is the RightHex above the heading 2 spine?
        if (DeltaCol < DeltaRow)
        {
            // The RightHex is above the heading 2 spine, but below the midline.
            return DeltaRow + (UnitArray[RightHex][0] - (UnitArray[LeftHex][0] + DeltaRow));
        }
        else
        {
            // The RightHex is below the heading 2 spine and to the right of LeftHex.
            if (mapOddUp)
            {
                if (IsColEven(UnitArray[LeftHex][1]))
                {
                    return DeltaCol + (Math.round(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
                else
                {
                    return DeltaCol + (Math.floor(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
            }
            else
            {
                if (IsColEven(UnitArray[LeftHex][1]))
                {
                    return DeltaCol + (Math.floor(DeltaCol / 2) - UnitArray[RightHex][1]);
                }
                else
                {
                    return DeltaCol + (Math.round(DeltaCol / 2) - UnitArray[RightHex][1]);
                }

            }
        }
    }
}

// This section is for the custom validation code.
// Modify this code as needed to address unique issues with the game orders or board.

function CheckAllOrders(MoveOrders)
{
    // Here is where you can validate the overall orders before they are processed.
    // If you have some reason to not like the way the orders are written, write the code here.
    
    // Set the value to false if you want to invalidate the entire collection of orders.
    return true;
}

function CheckValidMove(UnitNumber)
{
    // Here is where you can control how the internal portion of the map is designed.
    // If you want to limit access between two hexes, then you can check for them here.
    // UnitArray[UnitNumber] is the starting hex.  UnitArray[0] is the ending hex.
    
    // Set the value to false if you want to invalidate the move, otherwise return with a true.
    return true;
}

// Below this point are the support functions to manage the above functions.

function IsColEven(HexCol)
{
    // This function returns a true if the Hex's current row is even.
    // It is used by MoveStep() to determine the next hex.
    if(HexCol % 2 == 0)
    {
        return true;
    }
    else
    {
        return false;
    }
}

function MoveStep(UnitNumber, NextOrder)
{
    var HexCol = UnitArray[UnitNumber][0];
    var HexRow = UnitArray[UnitNumber][1];
    var HexFace = UnitArray[UnitNumber][2];
    
    switch(NextOrder)
    {
        case "L":
            if (mapIsUp0)
            {
                if (HexFace == 0)
                {
                    HexFace = 5;
                }
                else
                {
                    HexFace--;
                }
            }
            else
            {
                if (HexFace == 1)
                {
                    HexFace = 6;
                }
                else
                {
                    HexFace--;
                }
            }
            break;
        case "R":
            if (mapIsUp0)
            {
                if (HexFace == 5)
                {
                    HexFace = 0;
                }
                else
                {
                    HexFace++;
                }
            }
            else
            {
                if (HexFace == 6)
                {
                    HexFace = 1;
                }
                else
                {
                    HexFace++;
                }
            }
            break;
        case "F":
            switch(HexFace)
            {
                case 0:
                    HexRow--;
                    break;
                case 1:
                    HexCol++;
                    if ((IsColEven(HexCol) == false && mapOddUp) || (IsColEven(HexCol) && mapOddUp == false))
                    {
                        HexRow--;
                    }
                    break;
                case 2:
                    HexCol++;
                    if ((IsColEven(HexCol) && mapOddUp) || (IsColEven(HexCol) == false && mapOddUp == false))
                    {
                        HexRow++;
                    }
                    break;
                case 3:
                    HexRow++;
                    break;
                case 4:
                    HexCol--;
                    if ((IsColEven(HexCol) && mapOddUp) || (IsColEven(HexCol) == false && mapOddUp == false))
                    {
                        HexRow++;
                    }
                    break;
                case 5:
                    HexCol--;
                    if ((IsColEven(HexCol) == false && mapOddUp) || (IsColEven(HexCol) && mapOddUp == false))
                    {
                        HexRow--;
                    }
                    break;
                case 6:
                    HexRow--;
                    break;
            }
            break;
        case "B":
            switch (HexFace) 
            {
                case 0:
                    HexRow++;
                    break;
                case 1:
                    HexCol--;
                    if ((IsColEven(HexCol) && mapOddUp) || (IsColEven(HexCol) == false && mapOddUp == false)) 
                    {
                        HexRow++;
                    }
                    break;
                case 2:
                    HexCol--;
                    if ((IsColEven(HexCol) == false && mapOddUp) || (IsColEven(HexCol) && mapOddUp == false)) 
                    {
                        HexRow--;
                    }
                    break;
                case 3:
                    HexRow--;
                    break;
                case 4:
                    HexCol++;
                    if ((IsColEven(HexCol) == false && mapOddUp) || (IsColEven(HexCol) && mapOddUp == false)) 
                    {
                        HexRow--;
                    }
                    break;
                case 5:
                    HexCol++;
                    if ((IsColEven(HexCol) && mapOddUp) || (IsColEven(HexCol) == false && mapOddUp == false)) 
                    {
                        HexRow++;
                    }
                    break;
                case 6:
                    HexRow++;
                    break;
            }
            break;
        case "W":
            break;
        default:
    }

    // Now return the results in the "zero" element of the Unit Array
    UnitArray[0][0] = HexCol;
    UnitArray[0][1] = HexRow;
    UnitArray[0][2] = HexFace;
}

This should help you all in the creation of a strategy game... at least it's helping me...

Someone out there willing to translate it into AS3?

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers