Don't click here unless you want to be banned.

LSL Wiki : LibraryGoGame2

HomePage :: PageIndex :: RecentChanges :: RecentlyCommented :: UserSettings :: You are crawl411.us.archive.org
Continued from Go Game first page...

You should now have a GoButton, a GoJoinButton, a GoSensorGrid and a GoTileGrid. Create a prim, drop an instance of each of those objects into its inventory, then finally put in the following two scripts which contain the main game logic, and you should have yourself a working Go board.

First off is the GoGameLogicScript which contains most of the actual game logic for handling captures and the like.

// GoGameLogicScript
// Jonathan Shaftoe
// Contains most of the game logic code for the Go game. It's big and it's nasty. It uses recursion
// to do group capture detection, so can be slow.
// For usage, see GoGameMainScript

integer gGameSize = 9; // size of the game board, can be 9, 13 or 19

integer gBlackScore; // current scores (captured stones)
integer gWhiteScore;

integer gDeadWhite; // count of dead stones in endgame
integer gDeadBlack;

string gGameStatePrev; // Previous game state, used to check for Ko
string gGameState; // Game state - string representing the current state of the game board.
string gCurrentGame; // Another copy of the game state, used to store current state when working out
                    // captures.

list gGroup = []; // list of indexes of stones in (possibly captured) group.
integer gGroupLiberties; // How many liberties the currently being checked group has
integer gLibertyX; // coordinates of found liberty. We only care if there are no liberties, one liberty
integer gLibertyY; // or more than one (number if more than one doesn't matter), so we store the first
                   // found liberty here, then subsequently found liberties we check if they're the same
                   // as this one, and if not then we know we have more than one.
integer gSearchColour; // Are we looking for black or white stones

integer gTurn; // Whose turn is it, black or white
integer gLastWasPass;  // So we know if we get two passes in a row
integer gTurnPreEndgame;  // For turn handling during endgame

integer gGotAtari; // If we've found at least one group with only one liberty left after a stone
                   // is played, then we have an atari

list gToDelete; // list of indexes of stones to be removed due to capture (can be more than one group)

integer gGroupType; // type of group check being done, to avoid having to pass it through recursive calls.
// 0 - suicide check, 1 - normal turn check, 2 - endgame dead group marking, 3 - endgame scoring.
float gSize=4.0; // global scale factor.

set_player(integer turn, integer endgame, integer send_message) {
    gTurn = turn;
    if (send_message == 1) {
        llMessageLinked(LINK_ROOT, 101, (string)gTurn, (key)((string)endgame));
    }
}

integer get_board_state(integer x, integer y) {
    integer index = x + 1 + ((y + 1) * (gGameSize + 2));
    string num = llGetSubString(gGameState, index, index);
    return (integer)num;
}

set_board_state(integer x, integer y, integer newstate) {
    integer index = x + 1 + ((y + 1) * (gGameSize + 2));
    string before = llGetSubString(gGameState, 0, index - 1);
    string after = llGetSubString(gGameState, index + 1, llStringLength(gGameState) - 1);
    gGameState = before + (string)newstate + after;
}

// Sets gameboard size and initialises gameState. Note 3s used around edge to make
// group detection easier (no boundary conditions, 3 is neither 1 (black) or 2 (white))
set_size(integer size) {
    gGameSize = size;
    if (gGameSize == 9) {
        gGameState = "33333333333" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "30000000003" +
                     "33333333333"; 
    } else if (gGameSize == 13) {
        gGameState = "333333333333333" + 
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "300000000000003" +
                     "333333333333333";
    } else if (gGameSize == 19) {
        gGameState = "333333333333333333333" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "300000000000000000003" +
                     "333333333333333333333";
    }
}

// functions below for doing recursive group detection. This is NOT efficient.
// on a 19x19 board, you can run out of script execution stack space if there are big
// groups. Could be improved.

recurse_check_util(integer x, integer y, integer dir) {
    integer neighbour = get_board_state(x, y);
    if (neighbour == gSearchColour) {
        recurse_check(x, y, dir);
    } else if (neighbour == 0 && gGroupType != 2) {
        if (x != gLibertyX || y != gLibertyY) {
            gLibertyX = x;
            gLibertyY = y;
            gGroupLiberties++;
        }
    } else if ((neighbour == 2 || neighbour == 1) && gGroupType == 3) {
        if (gGroupLiberties != neighbour) {
            gGroupLiberties += neighbour;
        }
    }
}

recurse_check(integer x, integer y, integer dir) {
    if (gGroupType == 1 && gGroupLiberties >= 2) {
        return;
    }
    if (gGroupType == 0 && gGroupLiberties == 1) {
        return;
    }
    if (llListFindList(gGroup, [x + (y * gGameSize)]) != -1) {
        return;
    }
    gGroup = gGroup + [x + (y * gGameSize)];
    if (dir != 0) {
        recurse_check_util(x - 1, y, 1);
    }
    if (dir != 1) {
        recurse_check_util(x + 1, y, 0);
    }
    if (dir != 2) {
        recurse_check_util(x, y - 1, 3);
    }
    if (dir != 3) {
        recurse_check_util(x, y + 1, 2);
    }
}

integer check_captures(integer x, integer y, integer dir, integer type) {
    integer groupSize = 0;
    gGroup = [];
    gGroupLiberties = 0;
    gLibertyX = -1;
    gLibertyY = -1;
    gGroupType = type;
    recurse_check(x, y, dir);

// if type == 0 then we're checking for a suicide play.
    if (type == 3 || gGroupLiberties == 0) {
        groupSize = llGetListLength(gGroup);
    }
    if (type == 1) { // finding captured groups in normal gameplay
        if (gGroupLiberties == 1) {
            gGotAtari = 1;
        } else if (gGroupLiberties == 0) {
            integer i = 0;
            while (i < groupSize) {
                integer index = llList2Integer(gGroup, i);
                integer remx = index % gGameSize;
                integer remy = index / gGameSize;
                set_board_state(remx, remy, 0);
                i++;
            }
            gToDelete = gToDelete + gGroup;
        }
    } else if (type == 2) {  // marking dead groups in endgame 
        integer i = 0;
        while (i < groupSize) {
            integer index = llList2Integer(gGroup, i);
            integer remx = index % gGameSize;
            integer remy = index / gGameSize;
            set_board_state(remx, remy, 0);
            i++;
            gToDelete = gToDelete + gGroup;
        }        
    } else if (type == 3) { // counting territory space in endgame - gGroupLiberties used to store colour of neighbouring stones to work out territory owner. If both colours are found then they're added so result will be 3 (or more)
        integer i = 0;
        while (i < groupSize) {
            integer index = llList2Integer(gGroup, i);
            integer remx = index % gGameSize;
            integer remy = index / gGameSize;
            set_board_state(remx, remy, 3);
            i++;
        }
        if (gGroupLiberties == 1) {
            gBlackScore += groupSize;
        } else if (gGroupLiberties == 2) {
            gWhiteScore += groupSize;
        }        
    }
    
    return groupSize;
}

default {
    state_entry() {
        gBlackScore = 0;
        gWhiteScore = 0;
        gTurn = 0;
        gLastWasPass = FALSE;
        state playing;
    }
}

// Normal state during game play.
state playing {
    state_entry() {
    }

    link_message(integer sender, integer num, string str, key id) {
        if (num == 100) {
            set_size((integer)str);
            gSize = (float)((string)id);
        } else if (num == 4) {
            list params = llParseString2List(str, [","], []);
            integer x = llList2Integer(params, 0);
            integer y = llList2Integer(params, 1);
            if (get_board_state(x, y) != 0) {
                llWhisper(0, "Cannot play there, already occupied.");
                llMessageLinked(LINK_ALL_CHILDREN, 21, "", "");
            } else {
                gCurrentGame = gGameState;
                set_board_state(x, y, gTurn + 1);
                gSearchColour = (1 - gTurn) + 1;
                integer scorechange = 0;
                gGotAtari = 0;
                gToDelete = [];
                if (get_board_state(x + 1, y) == gSearchColour) {
                    scorechange += check_captures(x + 1, y, 0, 1);
                }
                if (get_board_state(x - 1, y) == gSearchColour) {
                    scorechange += check_captures(x - 1, y, 1, 1);
                }
                if (get_board_state(x, y + 1) == gSearchColour) {
                    scorechange += check_captures(x, y + 1, 2, 1);
                }
                if (get_board_state(x, y - 1) == gSearchColour) {
                    scorechange += check_captures(x, y - 1, 3, 1);
                }
                if (scorechange > 0 && gGameState == gGameStatePrev) {
                    llWhisper(0, "Cannot play there due to ko");
                    gGameState = gCurrentGame;
                    llMessageLinked(LINK_ALL_CHILDREN, 21, "", "");
                    return;
                }
                if (scorechange == 0) {
                    gSearchColour = gTurn + 1;
                    integer checkSuicide = check_captures(x, y, -1, 0);
                    if (checkSuicide > 0) {
                        llWhisper(0, "Cannot play there, suicide play.");
                        gGameState = gCurrentGame;
                        llMessageLinked(LINK_ALL_CHILDREN, 21, "", "");
                        return;
                    }
                }
                llMessageLinked(LINK_ROOT, 500, (string)x, (string)y);
                if (gGotAtari == 1) {
                    if (gTurn == 0) {
                        llWhisper(0, "Black says 'atari'");
                    } else {
                        llWhisper(0, "White says 'atari'");
                    }
                }
                gGameStatePrev = gCurrentGame;
                if (scorechange > 0) {
                    // let's actually do the removing now
                    integer i = 0;
                    integer delete_index;
                    integer remx;
                    integer remy;
                    while (i < scorechange) {
                        delete_index = llList2Integer(gToDelete, i);
                        remx = delete_index % gGameSize;
                        remy = delete_index / gGameSize;
                        llMessageLinked(LINK_ALL_CHILDREN, 201, (string)remx + "," + (string)remy + ",0", "");
                        i++;
                    }
                    llMessageLinked(LINK_ALL_CHILDREN, 202, "", "");
                    string piece;
                    if (scorechange == 1) {
                        piece = "piece";
                    } else {
                        piece = "pieces";
                    }
                    if (gTurn == 0) {
                        gBlackScore += scorechange;
                        llWhisper(0, "Black captured " + (string)scorechange + " " + piece);
                    } else {
                        gWhiteScore += scorechange;
                        llWhisper(0, "White captured " + (string)scorechange + " " + piece);
                    }
                    llMessageLinked(LINK_ROOT, 400, (string)scorechange, (string)gTurn);
                }                
                gLastWasPass = FALSE;
                set_player(1 - gTurn, 0, 1);
                llMessageLinked(LINK_ALL_CHILDREN, 201, (string)x + "," + (string)y + "," + (string)(2 - gTurn), "");
            }
        } else if (num == 10) {
            if (gTurn == 0) {
                llWhisper(0, "Black passes");
            } else {
                llWhisper(0, "White passes");
            }
            gTurn = 1 - gTurn;
            gGameStatePrev = gGameState;
            if (gLastWasPass == TRUE) {
                llWhisper(0, "Consecutive passes, entering endgame.");
                state endgame;
            }
            gLastWasPass = TRUE;
            set_player(gTurn, 0, 1);
        } else if (num == 999) {
            state default;
        }
    }
}

// State during endgame, when players must mark any dead groups belonging to the other player.

state endgame {
    state_entry() {
        llMessageLinked(LINK_ROOT, 102, "", "");
        gLastWasPass = FALSE;
        gTurnPreEndgame = gTurn;
        gCurrentGame = gGameState;
        gDeadBlack = 0;
        gDeadWhite = 0;
        set_player(gTurn, 1, 1);
    }
    
    link_message(integer sender, integer num, string str, key id) {
        if (num == 103) {
            set_player((integer)str, (integer)((string)id), 0);
        } else if (num == 4) {
            list params = llParseString2List(str, [","], []);
            integer x = llList2Integer(params, 0);
            integer y = llList2Integer(params, 1);
            if (get_board_state(x, y) != (1 - gTurn) + 1) {
                llWhisper(0, "Invalid selection. Select your opponent's groups which are dead.");
                llMessageLinked(LINK_ALL_CHILDREN, 21, "", "");
            } else {
                gSearchColour = (1 - gTurn) + 1;
                gToDelete = [];
                integer scorechange = check_captures(x, y, -1, 2);
                integer i = 0;
                integer delete_index;
                integer remx;
                integer remy;
                while (i < scorechange) {
                    delete_index = llList2Integer(gToDelete, i);
                    remx = delete_index % gGameSize;
                    remy = delete_index / gGameSize;
                    llMessageLinked(LINK_ALL_CHILDREN, 201, (string)remx + "," + (string)remy + ",0", "1");
                    i++;
                }
                llMessageLinked(LINK_ALL_CHILDREN, 202, "", "");
                if (gSearchColour == 1) {
                    gDeadBlack += scorechange;
                } else {
                    gDeadWhite += scorechange;
                }
                set_player(gTurn, 1, 1);
            }
        } else if (num == 104) {
            if (str == "It's fine") {
                llMessageLinked(LINK_ALL_CHILDREN, 203, "", "");
                if (gTurn == gTurnPreEndgame) {
                    state scoring;
                } else {
                    set_player(gTurn, 1, 1);
                }
            } else if (str == "Dispute") {
                llWhisper(0, "Dead groups disputed, resuming play.");
                gTurn = gTurnPreEndgame;
                set_player(gTurn, 0, 1);
                gGameState = gCurrentGame;
                llMessageLinked(LINK_ALL_CHILDREN, 15, "", "");
                state playing;
            }
        } else if (num == 999) {
            state default;
        }
    }
}

// Actually working out the final score. 

state scoring {
    state_entry() {
        llMessageLinked(LINK_SET, 105, "", "");
        llWhisper(0, "Calculating final scores. Please be patient ...\nWarning - large open areas on a large board can cause this to error due to lack of memory.");
        llSetText("Calculating final scores ...", <0, 0, 1>, 1.0);
        integer x;
        integer y;
        gSearchColour = 0;
        for (x = 0; x < gGameSize; x++) {
            for (y = 0; y < gGameSize; y++) {
                if (get_board_state(x, y) == 0) {
                    check_captures(x, y, -1, 3);
                }
            }
        }
        gBlackScore += gDeadWhite;
        gWhiteScore += gDeadBlack;
        llWhisper(0, "Final score, black: " + (string)gBlackScore + "  and white: " + (string)gWhiteScore);
        llSetText("Final score, black: " + (string)gBlackScore + "  and white: " + (string)gWhiteScore, <0, 0, 1>, 1.0);
        llMessageLinked(LINK_ROOT, 106, "", "");
    }
    
    link_message(integer sender, integer num, string str, key id) {
        if (num == 999) {
            state default;
        }
    }
}

And now for the GoGameMainScript, which does all the gluing together of all the other scripts, and handles players joining/leaving the game and the like.

// GoGameMainScript
// Jonathan Shaftoe
// This is the main controlling script which sorts out rezzing all the objects and moving
// them around and sending messages to the other bits and pieces. It's big and it's messy.
// To use, create a single prim, drop into it a previously created GoButton, GoJoinButton,
// GoSensorGrid and GoTileGrid, then drop in the GoGaneLogicScript, then finally drop in this
// script. If you want the info button to give out a notecard, create one called Go Info and
// drop that into the inventory of the prim too (actually, it'll probably error if you don't
// do this, so.. )

// Channel used for listens, used for dialogs.
integer CHANNEL = 85483;
// Size of go board, 9, 13 or 19. This is just what it initialises to, can be changed
// during game setup
integer gGameSize = 9;

// Store the current listener, so we can remove it.
integer gListener;
// This is a general scaling multiplier, allowing you either to create a small dinky Go Board, or a
// large one. The maximum size that will work is 9.0, as beyond that linking of things fails. I've
// not experimented with the smallest workable size.
float SIZE = 3.0;

// Keys and names of the players playing each colour
key gBlackPlayer;
key gWhitePlayer;
string gBlackName;
string gWhiteName;
// Current player, whose turn it is.
key gPlayer;

// How many sensors we have, and the size of grid being used.
integer gXSensors;
integer gYSensors;

// Current scores
integer gBlackScore = 0;
integer gWhiteScore = 0;

// Used for counting rezzes, so we know when everything has been rezzedd
integer gCountRez;

// Whose turn it is, 0 is black, 1 is white.
integer gTurn = 0;

// How long since last turn (for reset)
string gLastTurnDate;
float gLastTurnTime;

// for the alpha part of coords, so we know what to call each square in messages. Note
// absence of I to avoid confusion with 1.
string COORDS = "ABCDEFGHJKLMNOPQRST";


set_player(integer turn, integer endgame, integer send_message) {
    vector colour;
    integer score = 0;
    string name;
    gTurn = turn;
    if (gTurn == 0) {
        gPlayer = gBlackPlayer;
        score = gBlackScore;
        colour = <0, 0, 0>;
        name = gBlackName;
    } else {
        gPlayer = gWhitePlayer;
        score = gWhiteScore;
        colour = <1, 1, 1>;
        name = gWhiteName;
    }
    llMessageLinked(LINK_ALL_CHILDREN, 0, "", gPlayer);
    if (endgame == 0) {
        llSetText(name + "'s turn (prisoners: " + (string)score + ").", colour, 1.0);
    } else if (endgame == 1) {
        llSetText(name + "'s turn to remove dead groups (prisoners: " + (string)score + ").", colour, 1.0);
        llInstantMessage(gPlayer, "Select opponent's dead groups then click done.");
    }
    if (send_message == 1) {
        llMessageLinked(LINK_ROOT, 103, (string)gTurn, (string)endgame);
    }
    gLastTurnDate = llGetDate();
    gLastTurnTime = llGetGMTclock();
}

set_size(integer size) {
    gGameSize = size;
    if (gGameSize == 9) {
        gXSensors = 3;
        gYSensors = 3;
        llSetTexture("a9878863-732f-09af-b908-09070ea9b213", 0);
    } else if (gGameSize == 13) {
        gXSensors = 4;
        gYSensors = 4;
        llSetTexture("a0973434-028b-3323-b572-8347095e6c3c", 0);
    } else if (gGameSize == 19) {
        gXSensors = 4;
        gYSensors = 5;
        llSetTexture("9575b5cf-72ec-14bf-e400-320f9008bbd9", 0);
    }
    llSetScale(<(gGameSize + 1.0) * SIZE / gGameSize, (gGameSize + 1.0) * SIZE / gGameSize, 0.01 * SIZE>);
    llMessageLinked(LINK_ALL_CHILDREN, 3, (string)gGameSize + "," + (string)gXSensors + "," + (string)gYSensors + "," + (string)SIZE, "");
    llMessageLinked(LINK_ROOT, 100, (string)gGameSize, (string)SIZE);
}

// State we start in, but don't stay in long. Need permission to link to do set-up.
default {
    state_entry() {
        llSetPrimitiveParams([
            PRIM_TYPE, PRIM_TYPE_BOX, PRIM_HOLE_DEFAULT, <0, 1, 0>, 0.0, 
                  ZERO_VECTOR, <1, 1, 0>, ZERO_VECTOR,
            PRIM_SIZE, <1 * SIZE, 1 * SIZE, 0.01 * SIZE>,
            PRIM_TEXTURE, ALL_SIDES, "5748decc-f629-461c-9a36-a35a221fe21f", <1, 1, 0>,  <0, 0, 0>, 0.0,
            PRIM_COLOR, ALL_SIDES, <0.8, 0.6, 0.5>, 1.0
        ]);
        llSetObjectName("Jonathan's Go Board");
        llSetObjectDesc("Jonathan's Go Board");
        llSitTarget(ZERO_VECTOR, ZERO_ROTATION);
        llRequestPermissions(llGetOwner(), PERMISSION_CHANGE_LINKS);
    }

    run_time_permissions(integer perms) {
        llBreakAllLinks();
        state initialising;
    }
}

// Initialisation state, rezzing all the buttons we need
state initialising {
    state_entry() {
        gCountRez = 7;
        llRezObject("GoButton", llGetPos() + <SIZE * (0.5 + 0.2), SIZE * .45, SIZE * .02>, ZERO_VECTOR, llGetRot(), 1);
        llRezObject("GoButton", llGetPos() + <SIZE * (0.5 + 0.2), SIZE * .25, SIZE * .02>, ZERO_VECTOR, llGetRot(), 2);
        llRezObject("GoButton", llGetPos() + <SIZE * (0.5 + 0.2), SIZE * -.25, SIZE * .02>, ZERO_VECTOR, llGetRot(), 4);
        llRezObject("GoButton", llGetPos() + <SIZE * (0.5 + 0.2), SIZE * -.45, SIZE * .02>, ZERO_VECTOR, llGetRot(), 3);
        llRezObject("GoButton", llGetPos() + <SIZE * (0.5 + 0.2), 0, SIZE * .02>, ZERO_VECTOR, llGetRot(), 5);
        llRezObject("GoJoinButton", llGetPos() + <0, SIZE * 0.5, 0.1 * SIZE>, ZERO_VECTOR, llGetRot(), 1);
        llRezObject("GoJoinButton", llGetPos() + <0, SIZE * -0.5, 0.1 * SIZE>, ZERO_VECTOR, llGetRot(), 2);
    }

    object_rez(key id) {
        llCreateLink(id, TRUE);
        gCountRez--;
        if (gCountRez == 0) {
            llMessageLinked(LINK_ALL_CHILDREN, 1, (string)SIZE, "");
            state setup_sensors;
        }
    }
}   

// Initialization state 2, seting up the sensor and tile grids.
state setup_sensors {
    state_entry() {
        gCountRez = 2;

        llRezObject("GoSensorGrid", llGetPos() + <0, 0, 0.005 * SIZE + .01>, ZERO_VECTOR, llGetRot(), 0);
        llRezObject("GoTileGrid", llGetPos() + <0, 0, 0.005 * SIZE + 0.1>, ZERO_VECTOR, llGetRot(), 0);
    }
    
    object_rez(key id) {
        llCreateLink(id, TRUE);
        gCountRez--;
        if (gCountRez == 0) {
            set_size(9);
            state awaiting_start;
        }
    }
}

// All initialised, waiting for players to join the game, plus can change game board size.

state awaiting_start {
    state_entry() {
        llSetTouchText("Game Size");
        llSetText("Go Game - awaiting players", <0, 0, 0>, 1.0);
    }

    state_exit() {
        llSetTimerEvent(0);
    }

    touch_start(integer num) {
        llDialog(llDetectedKey(0), "Set size of Go game board.\nCurrently set to: " + (string)gGameSize + "x" + (string)gGameSize, ["9x9", "13x13", "19x19"], CHANNEL);
        llListenRemove(gListener);
        gListener = llListen(CHANNEL, "", llDetectedKey(0), "");
        // Make sure we timeout the listen, in case they ignore the dialog
        llSetTimerEvent(60 * 2);  
    }

    listen(integer channel, string name, key id, string message) {
        if (message == "9x9") {
          set_size(9);
        } else if (message == "13x13") {
          set_size(13);
        } else if (message == "19x19") {
          set_size(19);
        }
        llWhisper(0, "Go board set to size: " + (string)gGameSize + "x" + (string)gGameSize); 
        llListenRemove(gListener);
        llSetTimerEvent(0);
    }

    timer() {
        llListenRemove(gListener);
        llSetTimerEvent(0);
    }
     
    link_message(integer sender, integer num, string str, key id) {
        if (num == 1) {
            gBlackPlayer = id;
            gBlackName = llKey2Name(id);
            llWhisper(0, gBlackName + " will play black.");
        } else if (num == 2) {
            gWhitePlayer = id;
            gWhiteName = llKey2Name(id);
            llWhisper(0, gWhiteName + " will play white.");
        } else if (num == 12) {
            llWhisper(0, llKey2Name(id) + " resets the Go board.");
            state resetting;
        } else if (num == 14) {
            llGiveInventory(id, "Go Info");
        }
        if (num == 1 || num == 2) {
            if (gBlackPlayer != "" && gWhitePlayer != "") {
                llWhisper(0, "Game started.");
                set_player(0, 0, 1);
                state playing;
            }
        }
    }
}

// Game has started, two players are playing.
state playing {
    state_entry() {
        llSetTouchText("Undo zoom");
    }
 
    touch_end(integer num) {
        if (llDetectedKey(0) == gPlayer) {
            llMessageLinked(LINK_ALL_CHILDREN, 0, "", gPlayer);
        }
    }

    link_message(integer sender, integer num, string str, key id) {
        if (num == 101) {
            set_player((integer)str, (integer)((string)id), 0);
        } else if (num == 400) {
            integer turn = (integer)((string)id);
            integer score = (integer)str;
            if (turn == 0) {
                gBlackScore += score;
            } else {
                gWhiteScore += score;
            }
        } else if (num == 500) {
            integer x = (integer)str;
            integer y = (integer)((string)id);
            llPlaySound("a46a5924-5679-6483-233a-5b0b165ec477", 1.0);
            string player;
            if (gTurn == 0) {
                player = "Black";
            } else {
                player = "White";
            }
            string coord = llGetSubString(COORDS, x, x);
            llWhisper(0, player + " played at " + coord + ", " + (string)(y + 1));
        } else if (num == 102) {
            state endgame;
        } else if (num == 12) {
            string nowDate = llGetDate();
            float nowTime = llGetGMTclock();
            if (nowDate == gLastTurnDate && ((nowTime - gLastTurnTime) < 600)) {
                llInstantMessage(id, "You cannot reset the Go board, an active game is in progress. If you are playing and wanting to reset, resign first to end a game. If a game has been abandoned, then the board will be resettable if 10 minutes pass with no turn played.");
            } else {
                llDialog(id, "Are you sure you want to reset the board? The game in progress will be lost.", ["Reset", "Cancel"], CHANNEL);
                llListenRemove(gListener);
                gListener = llListen(CHANNEL, "", id, "");
                llSetTimerEvent(60 * 4);
            }
        } else if (num == 13) {
            if (id == gWhitePlayer || id == gBlackPlayer) {
                llDialog(id, "Are you sure you want to resign the game?", ["Resign", "Cancel"], CHANNEL);
                llListenRemove(gListener);
                gListener = llListen(CHANNEL, "", id, "");
                llSetTimerEvent(60 * 4);
            }
        } else if (num == 14) {
            llWhisper(0, "Game in progress between " + gBlackName + " (Black: current prisoners " + (string)gBlackScore + ") and " + gWhiteName + " (White, current prisoners: " + (string)gWhiteScore + ")");
            llGiveInventory(id, "Go Info");
        }
    }
    
    listen(integer channel, string name, key id, string message) {
        llListenRemove(gListener);
        llSetTimerEvent(0);
        if (message == "Resign") {
            string resignerName;
            string winnerName;
            if (id == gBlackPlayer) {
                resignerName = "Black";
                winnerName = "White";
            } else {
                resignerName = "White";
                winnerName = "Black";
            }
            llWhisper(0, resignerName + " has resigned. " + winnerName + " wins.");
            llSetText(resignerName + " resigns. " + winnerName + " wins.", <0, 0, 1>, 1.0);
            state gameover;
        } else if (message == "Reset") {
            llWhisper(0, name + " resets the Go board.");
            state resetting;
        }
    }
    
    timer() {
        llListenRemove(gListener);
        llSetTimerEvent(0);
    }
    
    state_exit() {
        llSetTimerEvent(0);
    }
}

// Two passes have occured, we're in endgame, players must mark oppositions dead groups (and then
// agree with what their opponent has marked as dead )

state endgame {
    state_entry() {
    }

    link_message(integer sender, integer num, string str, key id) {
        if (num == 11) {
            set_player(1 - gTurn, 2, 1);
            llDialog(gPlayer, "Are you happy with the groups your opponent has marked as dead?\nIf you dispute, play will resume.", ["It's fine", "Dispute"], CHANNEL);
            llListenRemove(gListener);
            gListener = llListen(CHANNEL, "", gPlayer, "");
            // Make sure we timeout the listen, in case they ignore the dialog
            llSetTimerEvent(60 * 4);  
        } else if (num == 105) {
            state scoring;
        } else if (num == 101) {
            set_player((integer)str, (integer)((string)id), 0);
        } else if (num == 12) {
            string nowDate = llGetDate();
            float nowTime = llGetGMTclock();
            if (nowDate == gLastTurnDate && ((nowTime - gLastTurnTime) < 600)) {
                llInstantMessage(id, "You cannot reset the Go board, an active game is in progress. If you are playing and wanting to reset, resign first to end a game. If a game has been abandoned, then the board will be resettable if 10 minutes pass with no turn played.");
            } else {
                llDialog(id, "Are you sure you want to reset the board? The game in progress will be lost.", ["Reset", "Cancel"], CHANNEL);
                llListenRemove(gListener);
                gListener = llListen(CHANNEL, "", id, "");
                llSetTimerEvent(60 * 4);
            }
        } else if (num == 14) {
            llWhisper(0, "Game in progress between " + gBlackName + " (Black: current prisoners " + (string)gBlackScore + ") and " + gWhiteName + " (White, current prisoners: " + (string)gWhiteScore + ") - players in endgame");
            llGiveInventory(id, "Go Info");
        }
    }

    listen(integer channel, string name, key id, string message) {
        llListenRemove(gListener);
        llSetTimerEvent(0);
        llMessageLinked(LINK_ROOT, 104, message, "");
        if (message == "Dispute") {
            state playing;
        } else if (message == "Reset") {
            llWhisper(0, name + " resets the Go board.");
            state resetting;
        }
    }
    
    touch_end(integer num) {
        if (llDetectedKey(0) == gPlayer) {
            llMessageLinked(LINK_ALL_CHILDREN, 0, "", gPlayer);
        }
    }
    
    timer() {
        llListenRemove(gListener);
        llSetTimerEvent(0);
    }
    
    state_exit() {
        llSetTimerEvent(0);
    }
}

// End game finished, waiting for GoGameLogic to work out final score.
state scoring {
    state_entry() {
    }

    link_message(integer sender, integer num, string str, key id) {
        if (num == 106) {
            state gameover;
        } else if (num == 14) {
            llWhisper(0, "Game in progress between " + gBlackName + " (Black: current prisoners " + (string)gBlackScore + ") and " + gWhiteName + " (White, current prisoners: " + (string)gWhiteScore + ") - game scoring.");
            llGiveInventory(id, "Go Info");
        }
    }
}

// Game finished and final score displayed
state gameover {
    state_entry() {
    }
    link_message(integer sender, integer num, string str, key id) {
        if (num == 12) {
            state resetting;
        } else if (num == 14) {
            llGiveInventory(id, "Go Info");
        }    
    }
}

// Reset board ready to start new game.
state resetting {
    state_entry() {
        llMessageLinked(LINK_SET, 999, "", "");
        set_size(gGameSize);
        gBlackPlayer = "";
        gWhitePlayer = "";
        gBlackScore = 0;
        gWhiteScore = 0;
        state awaiting_start;
    }
}

Here is the text of the Go Info notecard given out by the info button. This should be saved as a notecard called Go Info and dropped in the inventory of the go game prim.

Hello and welcome to Jonathan's Go game.

Go is an ancient game from Asia with simple rules but complex gameplay. Two players, white and black, take turns to place a stone on a board. If a group of one colour is completely surrounded by a group of another, it is removed from the board. Note that stones are placed on the junctions of lines, not in the spaces between them, and that diagonals do not count. Once both players agree that there is no further advantage in playing any further stones, the one with the greatest score wins. 

A full description of the rules of Go is beyond the scope of a notecard. There are many websites with descriptions and tutorials, for example:

  http://www.britgo.org/
  
There are two types of scoring in common use,  Japanese or Territory scoring, and Chinese or Area scoring. I've implemented Territory scoring. In practice there is little difference between the two.

Though the basic rules are implemented, there are some unusual circumstances which aren't covered, for example triple ko or seki. In the unlikely event of these occuring, you'll have to sort out how to handle it between yourselves.

I have designed the board to be hopefully as intuitive to use as possible. Before starting a game, the size of the game board can be set by clicking anywhere on the board and selecting from the three commonly used sizes on offer. A 9x9 game can be played relatively quickly, but loses the deeper levels of strategy involved in a larger game. It is a good choice for beginners. A 19x19 game is a proper full sized game, and can take upwards of two hours to complete. 13x13 is a midpoint between the two. 

Join a game by clicking one of the two join buttons. Once two players have joined, the game will start, and only those two players will be able to use the board until the game is finished, or the board is reset. The board can only be reset if no move has been played for a certain amount of time (currently set to 10 minutes) - this prevents boards being left in a used state.

Place a stone on the board by clicking first on the coloured translucent square over the area you want to play in, then on the actual square over the point you want to play. This two step process is to conserve the prim usage of the board. If after the first click you change your mind, you can 'unzoom' by clicking anywhere else on the board. If you try to play somewhere illegal, due to ko or suicide for example, the board will tell you.

Once both players have passed consecutively, the board enters end-game. During this stage, each player in turn is invited to select which of his or her opponent's stones are 'dead' - that is stones whose capture is inevitable and unpreventable, generally all groups lacking two distinct 'eyes' (holes inside the group). Selecting a stone will remove all of the stones in that connected group. Once each player has signified that they have completed doing this, by pressing 'Done', the other is asked if they agree with the stones removed. If not, they can dispute the endgame and play resumes. It is essential to remove dead groups, as the automatic scoring cannot otherwise work.  

Once both players have selected their opponent's dead groups and had their selection confirmed, final scoring is calculated. This can take a while, depending on the size of board and size of territory to be counted. Unfortunately due to the limits of LSL in unusual circumtances, for example on a large board with a large area of territory, this can cause an out of memory error in the script. Please do not enter endgame scoring on a sparsely populated board, use the 'resign' button instead.

Enjoy!

Jonathan Shaftoe

And finally, here's some documentation I did for myself on the many link messages used. If you're going to try to figure out how it all works or change the code, this will probably be invaluable!

// Messages
// 0. From MainScript to Button, Sensor - sets current player, black or white, and resets Sensor to zoom1
// 1. From MainScript, to JoinButton, Button. Sets SIZE
// 2. From MainScript, to ?. Sets SIZE and gGameSize (depricated?)
// 1 & 2 From JoinButton to MainScript - sets who's playing black/white
// 3. From MainScript, to Tile, Sensor, sets gGameSize, gX/YSensors and SIZE, sent with set_size
// 4. From Sensor to Logic, turn attempt made, coordinates passed.
// 5. From Sensor to Sensor. coordinates of zoom from zoom1 to zoom2.
// 10. From Button, to Logic, player pressed 'pass'
// 11. From Button to Main, player pressed 'done'
// 12. From Button to Main, player pressed 'reset'
// 13. From Button to Main, player pressed 'resign'
// 14. From Button to Main, player pressed 'info' 
// 15. From Logic, to Tile, endgame disputed, reset tile to previous state
// 20. From Sensor to Sensor, go to waiting state, move attempted
// 21. From Logic to Sensor, go back from waiting state to zoom2, attempt to move failed.
// 100. From Main to Logic, sets gGameSize and SIZE, sent with every set_size
// 101. From Logic, to Main, sets whose turn and whether we're in endgame.
// 102. From Logic to Main, entering endgame after two passes
// 103. From Main to Logic, sets current player/endgame state
// 104. From Main to Logic, whether player disputes endgame/dead groups or not
// 105. From Logic to Main and Sensors, doing scoring (so sensors hide)
// 106. From Logic to Main, from scoring to gameover
// 201. From Logic to Tile, display piece, x, y, colour (1, 2) passed (displays immediately) or 0 to remove (doesn't display until ..).
// 202. From Logic to Tile, display result of removes from 201 messages.
// 203. From Logic to Tile, clears backup states (for endgame disputes) as agreed.
// 301. From Tile to TileFace, do actual display for given state.
// 400. From Logic to Main, send new captures done by given player for score update
// 500. From Logic to Main, turn played, so do message and do sound
// 999. From Main, to JoinButton, Sensors, Tile, Logic - game reset.

// Main sends: 0, 1, 2, 3, 100, 103, 104, 999
// Main receives: 1, 2, 11, 12, 13, 14, 101, 102, 105, 106, 400, 500

// TileFace sends: none
// TileFace receives: 301

// Tile sends: 301
// Tile receives: 3, 15, 201, 202, 203, 999

// Logic sends: 15, 21, 101, 102, 105, 106, 201, 202, 203, 400, 500
// Logic receives: 4, 10, 100, 103, 104, 999

// Button sends: 10, 11, 12, 13, 14
// Button receives: 0, 1

// Sensor sends: 4, 5, 20
// Sensor receives: 0, 3, 5, 20, 21, 105, 999

// JoinButton sends: 1, 2
// JoinButton receives: 1, 999
There are 3 comments on this page. [Display comments/form]