2013/03/23

quickie

Even when everything and everyone conspires against you, you should do the utmost to deliver what you promised.

So, without a lot of explanation (that will follow early next week) the following links :
CRUD server 
CRUD server demo

This is no less than a general solution for the backend of most application demos that you see on the internet and one specific implementation.

Enjoy !  

2013/03/15

going gui

Last week the NetKernel repository got a new module (html5-frameworks) containing a couple of often used Javascript frameworks. This is not by accident. While HTML5 does contain the seeds to push back on Javascript in the browser, things are not yet at that stage (and won't be for some time to come).

NetKernel on the other hand is often positioned as a backend engine. And this is strange. A lot of great websites run on top of the Jetty webserver and NetKernel's Fulcrums do too. So there is absolutely no reason for the frontend-crowd not to use NetKernel as a serving platform. I myself have already done this in the past using the Qooxdoo framework. However, I am not a graphical designer. Making sexy interfaces is not my thing. This is however not NetKernel's fault !

A couple of weeks back somebody asked me to redo this presentation using NetKernel as the backend and serving the AngularJS framework on NetKernel as well. Challenging me is a great way to distract me from my - other - work ... I'm always up for challenges. Next week I'll show you the general backend solution for all such demo-applications (and the internet is full of them, every new framework has such showcases). Let it be known that I can now create similar applications in under 15 minutes. Applications that benefit from all the NetKernel optimizing too. Try beating that !

This week I'll show you a frontend solution for a new tool that is released today in Layer1. This tool (a list accessor for the golden threads in a system) is going to be used in one of my current projects to allow manual control over the expiry of some resources. This is a very common use case. 

Akamai for example does not only charge mint for their caching solution they also charge for every request to clear a certain cached object before the normal expiry moment. Obtaining the access from them to do it yourself is only in the range of very large businesses. Kerching ! 

My free solution to control golden threads (and thus expiry) manually can be found here. Update to the latest (today) Layer1, make sure you get the html5-frameworks module installed (AngularJS is served from there), install the module, browse to http://localhost:8080/goldenthread/interface/index and watch the combined power of NetKernel and AngularJS in action.

In the source you'll find a simple RestOverlay and less than 100 lines of Groovy code to provide the backend, less than 50 lines of Javascript code that drives the magic in the AngularJS enhanced HTML for the frontend. The bootstrap css makes it look ... maybe not sexy, but definitely not ugly either.

Enjoy !

P.S. This tool (adapted of course) will soon be added to the Backend Fulcrum tools of NetKernel itself. I'm offering it here as a showcase of quick to-market development.

P.P.S. If you want the equivalent of the Akamai solution inside your software and at a fraction of the cost, check out NetKernel !

2013/02/08

reuse

As happens so often at the start of the new year, at least a couple of the good intentions are quickly moved aside under the pressure of reality. In this case under the pressure of absolutely crazy (in a good way) things happening in the NetKernel space.

So let me talk a bit about reuse while you wait just a bit longer on the second episode of Pinky and the Brain. I'll start with a quote from Frank Boddeke : 
Most of our products are re-shuffles of modules we already have.
Sounds crazy ? Well, did you look at the periodic table recently ? I had to learn it by heart in secondary school and my knowledge is still - largely - relevant today. It was already largely relevant in 1896. I was in secondary school in the (nineteen)eighties. The number of new molecules that has been build with those same elements since then is huge (not the mention the number since the days of Mendeljev himself).

NetKernel works the same way. And the simile continues, for just as you can also compose molecules, everything is a resource and the composition of a resource is also a resource which can be use in a composition ...

Not convinced ? Well, recently I quickly required a set of resources, let us abstract it as res:/nameproject/resourceid. Now, as these things go, this is an infinite set of resources. A couple of these resolve to actual data, most do not. I needed a sensible default for those that don't.



Didn't I recently read about just that pattern ? Yes, I did ... in the Tic-Tac-Toe series. Here's the relevant bit of mapper configuration :


    <endpoint>
        <verbs>SOURCE,SINK,DELETE,EXISTS</verbs>
        <grammar>
            <simple>c:{x}:{y}</simple>
        </grammar>
        <request>
            <identifier>active:cell</identifier>
            <argument name="x">arg:x</argument>
            <argument name="y">arg:y</argument>
        </request>
    </endpoint>
    <endpoint>
        <verbs>SOURCE,SINK,DELETE,EXISTS</verbs>
        <grammar>
            <active>
                <identifier>active:cell</identifier>
                <argument name="x"/>
                <argument name="y"/>
            </active>
        </grammar>
        <request>
            <identifier>fpds:/ttt/cell/[[arg:x]]-[[arg:y]]</identifier>
            <representation>java.lang.String</representation>
        </request>
    </endpoint>
    <endpoint>
        <grammar>
            <simple>res:/ttt/cell/{x}-{y}</simple>
        </grammar>
        <request>
            <identifier>res:/ttt/cell/default</identifier>
        </request>
    </endpoint>


Still not convinced ? Well, recently (did I mention times are crazily busy ?) I was asked to design a central logging component for NetKernel. Not a problem, you just provide a java.util.logging.SocketHandler handler in your logging configuration. But where do the logs go to ? Well, you could surely use something like Splunk, but there are NetKernel patterns/molecules for it too !




The Enterprise Edition users could do it with NKP and seamlessly log straight on a (or multiple) central server(s). For the Standard Edition users ... remember Camel ?  In fact I blogged about it myself here.


 from("mina:tcp://0.0.0.0:" + vPort + "?textline=true&sync=false")
 .to("class:org.elbeesee.logging.consumer.endpoint.LogMessageHandler?method=onMessage");


And that's it. Five minutes to pull together a module that provides a central logging server.

Enjoy and watch this space for more soon !

2013/01/11

pinky and the brain

It is all very well, these Phenomenal cosmic powers in an itty bitty Netkernel, but where do you start when you wake up screaming at 3 am in the morning, sweat dripping and a head full with this one plan to conquer the world ?

Note : If the above never happens to you ... you are a lucky person, trust me !

After the obligatory Muhahaha, you fire up your computer, start NetKernel, ...

Fifteen minutes later you are playing Minecraft or Dwarven Fortress and an hour after that you are back in bed, hoping that the alarm will not go off at 6 am (it will).

The problem wasn't that your plan/vision/idea was bad, but that you didn't quite have the tools ready in NetKernel to build it. You got distracted in creating those, next thing you were looking for stuff on the internet ... <fill in the rest of the slippery slope yourself>.

So, this week (and in future posts too) I'm going to provide a couple of common tools for building a service on the web and next week ... we are going to build one.

JQuery is a good place to start. We can debate whether or not it is worth serving it in NetKernel ... I think it is. So here is my module to do so.

Next on my tool list is a framework to build a decent HTML5 webpage. I like Skeleton. And here is my module to serve it.

Typical in a service on the web these days is that there is a free tier or an online working demo. That does open a door to abuse of course and since we - for the time being - live in a dog-eat-dog world, it is best to take this into account and have some kind of protection against it.

My brother-in-law, an excellent web/graphical designer, tells me it is both old hat and annoying for potential customers, but I like to use Recaptcha. Yes, another Google-capt(ch)ure . There is already an existing implementation by Chris Cormack, available on the NetKernelROC GitHub. Alternatively, here is mine (different approach, same result).

There all that is left now is to build a killer application and a teaser website. Come and see next week (or build your own at 3 am) !

P.S. I'll be visiting the Twin Cities again next week. If you want to meet-up, ping me !

2012/12/23

interesting times

Only a year ago I was sitting home on leave after telling some bigwigs at the company exactly what I thought of them and of the way they were running things. Bit like this, with about as much drama, just as true, less funny though. And I had been working in the company - a retailer based in Belgium - for 18+ years.

A year later I have published a book @O'Reilly, am running my own consultancy company, am working as consultant for two big companies and have taught a course of NetKernel at a third.

I have chinos and shirts in my cupboard now and some of these actually do have a colour/color other than black. I don't tuck the shirts in yet, but as the belly I got from the good life is receding, that will soon become an option. I now own a pair of decent looking shoes too.

Most of the time I work from a shared office located across from my son's school. Occasionally I have to go see my customers and mentors though, so I spent time in Minneapolis, Brussels, London and even Chipping Sodbury over the past year.

Professionally I've learned to do whole infrastructure setups with Amazon Web Services, am ever more proficient at Resource Oriented Computing and NetKernel and I am learning very quickly about linked data.


It wasn't all roses the past year.

Resistance to change is big, even when the change is the right thing to do, even when you can prove that the change is the right thing to do. That brings frustration and I don't have the I don't care but I will take your money attitude that I see many other employees/consultants/freelancers have. Nor will I ever have it.

Planes are a shitty (if unavoidable) way to travel. Actually jetlag doesn't really bother me, but when I get off my ears are only one step away from bleeding and it takes a lot of paracetamol to clear the killer headache. My legs take about two days to un-cramp. Very weird, the general population gets wider and taller, the allotted space on planes goes the other way. I was so wasted on one of these trips that I had myself talked into taking a BMW X5 as a rental car rather than a standard model. Didn't make a lot of money on that trip (I paid for that expense myself, like I said, I do care).

Also, I underwrite the following (do not click this link if you are easily offended) manifesto. Although with Resource Oriented Computing that should be developing rather than programming. Process (in whatever form) is a means, not an end in itself. If you think (or act) otherwise, you are on a collision course with me. I don't stick up companies (even if they don't care) for more time than I need to deliver the goods.


All in all I agree with Peter Rodgers when he says that it has not been a bad year. Not at all. Although I was raised in an environment where a 10+/10 was considered barely enough and might get some remarks if the teacher had too obviously been a bit lax in the marking job. There is always a lot of room for improvement. But not a bad year, not bad at all.

For the next year I have the good intention to start blogging about my ROC-adventures on a weekly basis again and continue changing the world (which seems to have survived the Mayan equivalent of the year 2000) one bit at a time.

2012/11/30

in memory of pds

These are busy times, the end of the world is coming closer quickly. Maybe I should go out on the streets with a big sign stating Repent sinners before it is too late ! Be saved, use NetKernel !

Actually the Mayans didn't say anything about the end of the world, but it would definitely be one of the more interesting publicity campaigns.

Right, no more philosophy this week. In his Tic-Tac-Toe series Peter Rodgers bumps into the problem that the available in memory implementation of the PDS accessor doesn't quite implement all the functionality that a PDS accessor should have. He then quickly skips to using the H2 backed implementation.

While that of course works (the H2 implementation) I noticed in my Connect Four implementation that things are not as snappy as they should be for a game. Especially if the board gets bigger, the game becomes database bound.

So, here is my implementation of the in memory PDS accessor. It uses the golden thread pattern for expiry. Enjoy !

// The usual suspects for an accessor
import org.netkernel.layer0.meta.impl.SourcedArgumentMetaImpl;
import org.netkernel.layer0.nkf.*;
import org.netkernel.module.standard.endpoint.StandardAccessorImpl;

// Processing
import org.netkernel.layer0.representation.IHDSNode;
import org.netkernel.layer0.representation.IHDSNodeList;
import org.netkernel.layer0.representation.IReadableBinaryStreamRepresentation;
import org.netkernel.layer0.representation.impl.HDSBuilder;
import org.netkernel.request.IRequestResponseFields;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryPDSAccessor extends StandardAccessorImpl {
    private static ConcurrentHashMap<String, InMemoryResource> mResources;
   
    private static class InMemoryResource {
        private final Object mRepresentation;
        private final IRequestResponseFields mUserMetaData;

        @SuppressWarnings("rawtypes")
        public InMemoryResource(INKFResponseReadOnly aResponse) {      
            if (aResponse != null) {  
                mRepresentation = aResponse.getRepresentation();
                mUserMetaData = aResponse.getHeaders();
            }
            else {  
                mRepresentation = null;
                mUserMetaData = null;
            }          
        }
      
        public Object getRepresentation() {  
            return mRepresentation;
        }
      
        public IRequestResponseFields getUserMetaData(){
            return mUserMetaData;
        }
    }

    public static class PDSArguments {
        public String mInstance;
        public String mZone;
        public String mIdentifier;
      
        private PDSArguments(INKFRequestContext aContext) throws NKFException {
            // Verify instance
            if (aContext.getThisRequest().argumentExists("instance")) {
                mInstance = aContext.getThisRequest().getArgumentValue("instance");
                if (mInstance.equals("pbv:instance")) {
                    mInstance = aContext.source("arg:instance",String.class);
                }
            }
            else {
                throw new NKFException("request does not have the required - instance - argument");
            }

            // Verify zone          
            if (aContext.getThisRequest().argumentExists("zone")) {
                mZone = aContext.getThisRequest().getArgumentValue("zone");
                if (mZone.equals("pbv:zone")) {
                    mZone = aContext.source("arg:zone",String.class);
                }
            }
            else {
                throw new NKFException("request does not have the required - zone - argument");
            }
            if (mZone.equals("")) {
                throw new NKFException("request does not have a valid - zone - argument");
            }          
          
            // Verify identifier
            if (aContext.getThisRequest().argumentExists("pds")) {
                mIdentifier = aContext.getThisRequest().getArgumentValue("pds");
                if (mIdentifier.equals("pbv:pds")) {
                    mIdentifier = aContext.source("arg:pds",String.class);
                }
                if (! mIdentifier.startsWith("/")) {
                    mIdentifier = "/" + mIdentifier;
                }
            }
            else {
                throw new NKFException("request does not have the required - identifier - argument");
            }
            if (mIdentifier.equals("")) {
                throw new NKFException("request does not have a valid - identifier - argument");
            }
        }

        private PDSArguments(String aInstance, String aZone, String aIdentifier) {
            mInstance = aInstance;
            mZone = aZone;
            mIdentifier = aIdentifier;
        }
      
        public Boolean isSet() {
            return mIdentifier.endsWith("/");
        }
      
        public String getCombined() {
            return mInstance + ":" + mZone + ":" + mIdentifier;
        }
      
        public String getGoldenThread() {
            return "gt:pds:" + mInstance + ":" + mZone + ":" + mIdentifier;
        }
      
        public String getIdentifier() {
            return mIdentifier;
        }

        public String getZone() {
            return mZone;
        }      

        public String getInstance() {
            return mInstance;
        }

        public boolean equals(Object aObject) {
            boolean vResult=false;
          
            if (aObject instanceof PDSArguments) {
                PDSArguments vOther = (PDSArguments) aObject;
                vResult = (mIdentifier.equals(vOther.mIdentifier) && mZone.equals(vOther.mZone) && mInstance.equals(vOther.mInstance));
            }
            return vResult;          
        }
      
        public int hashCode() {
            return mInstance.hashCode() ^ mZone.hashCode() ^ mIdentifier.hashCode();
        }
    }

    public InMemoryPDSAccessor() {
        this.declareThreadSafe();
        this.declareSourceRepresentation(IReadableBinaryStreamRepresentation.class);
        this.declareSourceRepresentation(IHDSNode.class);
        this.declareInhibitCheckForBadExpirationOnMutableResource();
        this.declareArgument(new SourcedArgumentMetaImpl("instance",null,null,new Class[] {String.class}));
        this.declareArgument(new SourcedArgumentMetaImpl("zone",null,null,new Class[] {String.class}));
        this.declareArgument(new SourcedArgumentMetaImpl("pds",null,null,new Class[] {String.class}));
        mResources = new ConcurrentHashMap<String, InMemoryResource>();
    }

    public void onSource(INKFRequestContext aContext) throws Exception {
        // SOURCE requires three arguments, instance zone and pds
      
        PDSArguments aArguments = new PDSArguments(aContext);
      
        if (aArguments.isSet()) {
            HDSBuilder vSet = new HDSBuilder();
            vSet.pushNode("set");

            for(Map.Entry<String, InMemoryResource> vEntry: mResources.entrySet()) {
                if (vEntry.getKey().startsWith(aArguments.getCombined())) {
                    int i = vEntry.getKey().indexOf(aArguments.getIdentifier());
                    String vPDS = vEntry.getKey().substring(i);
                    vSet.addNode("identifier", "pds:" + vPDS);          
                    vSet.pushNode("pds");
                    vSet.addNode("instance", aArguments.getInstance());
                    vSet.addNode("zone", aArguments.getZone());
                    vSet.addNode("pds", vPDS);
                    vSet.popNode();
                }
            }

            vSet.popNode();
          
            INKFRequest subrequest = aContext.createRequest("active:attachGoldenThread");
            subrequest.addArgument("id", aArguments.getGoldenThread());      
            aContext.issueRequest(subrequest);
          
            aContext.createResponseFrom(vSet.getRoot());
        }
        else {
            InMemoryResource vResource = mResources.get(aArguments.getCombined());
          
            if (vResource != null) {
                INKFRequest subrequest = aContext.createRequest("active:attachGoldenThread");
                subrequest.addArgument("id", aArguments.getGoldenThread());      
                aContext.issueRequest(subrequest);

                INKFResponse vResponse = aContext.createResponseFrom(vResource.getRepresentation());
                if (vResource.getUserMetaData() != null) {
                    vResponse.setHeaders(vResource.getUserMetaData());
                }
            }
            else {
                // default response is null
            }          
        }
    }
   
    public void onExists(INKFRequestContext aContext) throws Exception {
        // SOURCE requires three arguments, instance zone and pds
      
        PDSArguments aArguments = new PDSArguments(aContext);

        INKFResponse vResponse;

        if (aArguments.isSet()) {
            Boolean vResult = false;
            for(Map.Entry<String, InMemoryResource> vEntry: mResources.entrySet()) {
                if (vEntry.getKey().startsWith(aArguments.getCombined())) {
                    vResult = true;
                }
            }
            vResponse = aContext.createResponseFrom(vResult);
            if (vResult) {              
                INKFRequest subrequest = aContext.createRequest("active:attachGoldenThread");
                subrequest.addArgument("id", aArguments.getGoldenThread());      
                aContext.issueRequest(subrequest);
            }
            else {
                vResponse.setExpiry(INKFResponse.EXPIRY_ALWAYS);                          
            }
        }
        else {
            if (mResources.containsKey(aArguments.getCombined())) {
              
                INKFRequest subrequest = aContext.createRequest("active:attachGoldenThread");
                subrequest.addArgument("id", aArguments.getGoldenThread());      
                aContext.issueRequest(subrequest);
              
                vResponse = aContext.createResponseFrom(true);          
            }
            else {
                vResponse = aContext.createResponseFrom(false);
                vResponse.setExpiry(INKFResponse.EXPIRY_ALWAYS);          
            }          
        }

    }
   
    public synchronized void onSink(INKFRequestContext aContext) throws Exception {
        // Important : the onSink is synchronized ... only one at a time
        // SINK requires three arguments, instance, zone and pds and will persist
        // the primary argument
      
        PDSArguments aArguments = new PDSArguments(aContext);

        if (aArguments.isSet()) {
            throw new NKFException("unable to SINK to a set");
        }

        INKFRequest subrequest = aContext.createRequest("active:cutGoldenThread");
        subrequest.addArgument("id", aArguments.getGoldenThread());      
        aContext.issueRequest(subrequest);

        @SuppressWarnings("rawtypes")
        INKFResponseReadOnly vPrimary = aContext.getThisRequest().getPrimaryAsResponse();
        InMemoryResource vResource = new InMemoryResource(vPrimary);
        mResources.put(aArguments.getCombined(), vResource);
    }
   
    public void onDelete(INKFRequestContext aContext) throws Exception {
        // SOURCE requires three arguments, instance zone and pds
      
        PDSArguments aArguments = new PDSArguments(aContext);

        if (aArguments.isSet()) {
            IHDSNode vSet = null;
          
            INKFRequest subrequest = aContext.createRequest("active:pds");
            subrequest.addArgument("instance", aArguments.getInstance());
            subrequest.addArgument("zone", aArguments.getZone());
            subrequest.addArgument("pds", aArguments.getIdentifier());
            subrequest.setVerb(INKFRequestReadOnly.VERB_SOURCE);
            subrequest.setRepresentationClass(IHDSNode.class);
          
            vSet = (IHDSNode)aContext.issueRequest(subrequest);
            IHDSNodeList vNodes = vSet.getNodes("/set/pds");
            for (IHDSNode vNode : vNodes) {
                subrequest = aContext.createRequest("active:pds");
                subrequest.addArgument("instance", aArguments.getInstance());
                subrequest.addArgument("zone", aArguments.getZone());
                subrequest.addArgumentByValue("pds", vNode.getFirstValue("pds"));
                subrequest.setVerb(INKFRequestReadOnly.VERB_DELETE);
                subrequest.setRepresentationClass(Boolean.class);
                aContext.issueRequest(subrequest);
            }
            subrequest = aContext.createRequest("active:cutGoldenThread");
            subrequest.addArgument("id", aArguments.getGoldenThread());      
            aContext.issueRequest(subrequest);
          
            INKFResponse vResponse = aContext.createResponseFrom((vNodes != null));
            vResponse.setExpiry(INKFResponse.EXPIRY_ALWAYS);                      
        }
        else {
            INKFRequest subrequest = aContext.createRequest("active:cutGoldenThread");
            subrequest.addArgument("id", aArguments.getGoldenThread());      
            aContext.issueRequest(subrequest);
          
            InMemoryResource vResource = mResources.remove(aArguments.getCombined());
          
            INKFResponse vResponse = aContext.createResponseFrom((vResource != null));
            vResponse.setExpiry(INKFResponse.EXPIRY_ALWAYS);          
        }
    }
}

 

2012/11/01

from tic-tac-toe to connect four

The only way to find out if something is merely noise or actual information is sit down, read it and try what it says. Yourself. They named me Tom for a reason.

So last weekend I sat down, started reading [part1] of the Tic-Tac-Toe (TTT) example in the 1060 Research newsletters and set myself the goal of using it to create a Connect Four game.


There are a couple of differences :

The board is bigger. Typically Connect Four is being played on a 6 (rows) x 7 (columns) board, but variations exist and I wanted to have as general a solution as I could get. This resulted in two resources :
res:/connectfour/rows
res:/connectfour/columns
These can be backed by anything (a literal, a fileset, a database implementation), but the point is that they return the dimensions of the board.

There are more diagonals in play. While having a general solution for rows and columns, Peter rather quickly glosses over the diagonals in the TTT-solution, just defining the two that count (one of which is actually an antidiagonal).

I decided to have a more general solution where a diagonal goes from top left downwards. The diagonal:0 starts in the top left corner. Diagonals above that one get a positive index (diagonal:1 and so on, counting to the right), diagonals underneath that one get a negative index (diagonal:-1 and so on, counting down).

Antidiagonals go from top right downwards. The antidiagonal:0 starts in the the top right corner.  Antidiagonals above that one get a positive index (antidiagonal:1 and so on, counting to the left), antidiagonals underneath that one get a negative index (antidiagonal:-1 and so on, counting down).  

A token drops down the column
. As you know, a token (I went with X and O again) is not put in a specific place, but drops down the column into the lowest available position.


The victory condition is four of the same token next to each other in a row, column, diagonal or antidiagonal
. That after all is the name of the game ...



All that resulted in a bit more code. Since the dimensions of the board are unknown, I couldn't just map a row, column, diagonal and antidiagonal to a cell{} resource. Here for example is my groovy code for the row :


import org.netkernel.layer0.nkf.*;

INKFRequestContext aContext = (INKFRequestContext)context;

int vNumberOfRows = aContext.source("res:/connectfour/rows", Integer.class);
int vNumberOfColumns = aContext.source("res:/connectfour/columns", Integer.class);

int vRow = Integer.parseInt(aContext.getThisRequest().getArgumentValue("x"));

String vIdentifier = "cells{";


if ( (vRow >=0) && (vRow < vNumberOfRows)) {

  for (int vColumn = 0; vColumn < vNumberOfColumns; vColumn++) {
    vIdentifier = vIdentifier + "c:" + vRow + ":" + vColumn + ",";
  }
}
else {

  throw new NKFException("argument - x - should be in the range [0 - " + (vNumberOfRows - 1) + "]");
}

vIdentifier = vIdentifier + "}";

INKFRequest subrequest = aContext.createRequest(vIdentifier);
subrequest.setVerb(INKFRequestReadOnly.VERB_SOURCE);

aContext.createResponseFrom(aContext.issueRequestForResponse(subrequest));


In case you are wondering why I take context and put it into aContext ... that works a lot easier in my editor.

Obviously, the SINK to a cell also requires a bit of code :

case INKFRequestReadOnly.VERB_SINK:
  int i;
  for(i = vNumberOfRows - 1; i >= 0; i--) {
    if (! (aContext.exists("pds:/connectfour/cell/" + i + "-" + vColumn) ) ) {
      aContext.sink("pds:/connectfour/cell/" + i + "-" + vColumn, aContext.sourcePrimary(String.class));

      aContext.sink("pds:/connectfour/lastmove", "c:" + i + ":" + vColumn + ":" + aContext.sourcePrimary(String.class));

      break;
    }
  }
  if (i < 0) {
    NKFException e = new NKFException("invalid move");
    aContext.createResponseFrom(e);
  }
  break;

This also shows that I keep track of the last move made, a very convenient resource, since in order to check if there's a win, I need to know where the token fell.


Now, it is very easy to stand on the shoulders of a giant and shout out how great you are ... while ignoring the giant (which then proceeds to punch you in the face). However, given the above variations ... it was plain sailing all the way. The TTT story was great information, it delivers the goods !

For those of you not on Facebook, the amount of very (!) popular games that are mere variations (!) on this theme (with a bit of graphical polish) is huge. Just looking [here], I note Bubble Safari, Bubble Witch Saga, Diamond Dash, Candy Crush Saga, Bejeweled Blitz, Bubble Island. Look at the numbers. Look again, you missed some zeroes the first time.

Now, I'm not a graphical wizard (know thy strenghts and weaknesses), but when showing my first cut of the game to a friend, she wanted graphical icons instead of the textual X's and O's. I also added a multigame layer so it becomes a hotseat game that you can play with your loved ones. You can reach the (early) Christmas edition online here : http://netkernelbook.org/connectfour/<youridentifier>/

Obviously you need to replace <youridentifier> with some number or word or whatever that only you know.


A couple of quid pro quos to end this blogentry :

* A real multiplayer version is coming up soon. I'm thinking about using websockets and you can see that lastmove-resource is going to be very handy.
* I noticed as well as you - when you try it - that it is dreamily slow, not blindingly fast. This is due to the persistance mechanism being in a database (remember Peter switched to database persistance) rather than in memory. For a TTT that's sufficient, for larger boards that works too slow. I'm working on a fix for that. [Here] you can have a look at the Visualizer trace for a first move (with cleared cache). It does add up.
* Note that I only have a small host out there, it is a showcase host, not a massive player host. If you are interested in your own copy, contact me and I'll send you the full source.