Skip to main content
3 of 8
added 23267 characters in body
user avatar
user avatar

C++11 - nearly working :)

On my modest PC it seems to be able to twist over 4 million snakes per minute, but just a few reptiles seem to manage to crawl under the radar.

#include <cmath>
#include <cassert>
#include <ctime>
#include <sstream>
#include <vector>
#include <map>
#include <unordered_set>
#include <algorithm> // sort

using namespace std;

// theroretical max snake lenght (but the Sun would burn out before you'll reach that)
#define MAX_LENGTH ((int)(1+8*sizeof(unsigned)))
    
#ifndef QT_DEBUG // using Qt IDE for g++ builds
#ifndef NDEBUG
#define NDEBUG
#endif
#endif

#ifdef NDEBUG
inline void tprintf(const char *, ...){}
#else
#define tprintf printf
#endif

void panic(const char * msg)
{
    printf("PANIC: %s\n", msg);
    exit(-1);
}

// ============================================================================
// fast bit reversal
// ============================================================================
unsigned bit_reverse(register unsigned x, unsigned len)
{
    x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
    x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
    x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
    x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
    return((x >> 16) | (x << 16)) >> (32-len);
}

// ============================================================================
// 2D geometry (restricted to integer coordinates and right angle rotations)
// ============================================================================

// points using integer- or float-valued coordinates
template<typename T>struct tTypedPoint;

typedef int    tCoord;
typedef double tFloatCoord;

typedef tTypedPoint<tCoord> tPoint;
typedef tTypedPoint<tFloatCoord>  tFloatPoint;

template <typename T>
struct tTypedPoint {
    T x, y;

    template<typename U> tTypedPoint(const tTypedPoint<U>& from) : x((T)from.x), y((T)from.y) {} // conversion constructor

    tTypedPoint() {}
    tTypedPoint(T x, T y) : x(x), y(y) {}
    tTypedPoint(const tTypedPoint & p) { *this = p; }
    tTypedPoint operator+ (const tTypedPoint & p) const { return{ x + p.x, y + p.y }; }
    tTypedPoint operator- (const tTypedPoint & p) const { return{ x - p.x, y - p.y }; }
    tTypedPoint operator* (T scalar) const { return{ x * scalar, y * scalar }; }
    tTypedPoint operator/ (T scalar) const { return{ x / scalar, y / scalar }; }
    bool operator== (const tTypedPoint & p) const { return x == p.x && y == p.y; }
    bool operator!= (const tTypedPoint & p) const { return !operator==(p); }
    T dot(const tTypedPoint &p) const { return x*p.x + y * p.y; } // dot product  
    int cross(const tTypedPoint &p) const { return x*p.y - y * p.x; } // z component of cross product
    T norm2(void) const { return dot(*this); }

    // works only with direction = 1 (90° right) or -1 (90° left)
    tTypedPoint rotate(int direction) const { return{ direction * y, -direction * x }; }
    tTypedPoint rotate(int direction, const tTypedPoint & center) const { return (*this - center).rotate(direction) + center; }

    // used to compute the length of a ragdoll snake segment
    unsigned manhattan_distance(const tPoint & p) const { return abs(x-p.x) + abs(y-p.y); }
};


struct tArc {
    tPoint c;                        // circle center
    tFloatPoint middle_vector;       // vector splitting the arc in half
    tCoord      middle_vector_norm2; // precomputed for speed
    tFloatCoord dp_limit;            // dot product of point with the middle vector 
                                     // will be >= dp_limit if the point is inside the arc

    tArc() {}
    tArc(tPoint c, tPoint p, int direction) : c(c)
    {
        tPoint r = p - c;
        tPoint end = r.rotate(direction);
        middle_vector = ((tFloatPoint)(r+end)) / sqrt(2);
        middle_vector_norm2 = r.norm2();
        dp_limit = r.dot(middle_vector);
    }

    bool contains(tFloatPoint p) // p must be a point on the circle
    {
        if ((p-c).dot(middle_vector) >= dp_limit)
        {
            return true;
        }
        else return false;
    }
};

// returns the point of line (p1 p2) that is closest to c
// handles degenerate case p1 = p2
tPoint line_closest_point(tPoint p1, tPoint p2, tPoint c)
{
    if (p1 == p2) return{ p1.x, p1.y };
    tPoint p1p2 = p2 - p1;
    tPoint p1c =  c  - p1;
    return p1 + p1p2 * (p1c.dot(p1p2) / p1p2.norm2());
}

// variant of closest point computation that checks if the projection falls within the segment
bool closest_point_within(tPoint p1, tPoint p2, tPoint c, tPoint & res)
{
    tPoint p1p2 = p2 - p1;
    tPoint p1c = c - p1;
    tCoord nk = p1c.dot(p1p2);
    if (nk <= 0) return false;
    tCoord n = p1p2.norm2();
    if (nk >= n) return false;
    res = p1 + p1p2 * (nk / n);
    return true;
}

// tests intersection of line (p1 p2) with an arc
bool inter_seg_arc(tPoint p1, tPoint p2, tArc arc)
{
    tPoint m = line_closest_point(p1, p2, arc.c);
    tCoord r2 = arc.middle_vector_norm2;
    tPoint cm = m - arc.c;
    tCoord h2 = cm.norm2();
    if (r2 < h2) return false; // no circle intersection

    tPoint p1p2 = p2 - p1;
    tCoord n2p1p2 = p1p2.norm2();

    // works because by construction p is on (p1 p2)
    auto in_segment = [&](const tFloatPoint & p) -> bool
    {
        tFloatCoord nk = p1p2.dot(p - p1);
        return nk >= 0 && nk <= n2p1p2;
    };

    if (r2 == h2) return arc.contains(m) && in_segment(m); // tangent intersection

    //if (p1 == p2) return false; // degenerate segment located inside circle
    assert(p1 != p2);

    tFloatPoint u = (tFloatPoint)p1p2 * sqrt((r2-h2)/n2p1p2); // displacement on (p1 p2) from m to one intersection point

    tFloatPoint i1 = m + u;
    if    (arc.contains(i1) && in_segment(i1)) return true;
    tFloatPoint i2 = m - u;
    return arc.contains(i2) && in_segment(i2);
}

// test intersection of segments [a1 b1] and [a2 b2]
// segments may degenerate to points
bool inter_seg_seg(tPoint a1, tPoint b1, tPoint a2, tPoint b2)
{
    tPoint u1 = b1 - a1;
    tPoint u2 = b2 - a2;
    tCoord det = u1.cross(u2);
    tPoint link = a2 - a1;
    if (det == 0) // parallel
    {
        if (link.cross(u1) != 0) return false; // disjoint

        // 1st range bounds are not ordered while 2nd range's are
        auto ranges_overlap = [](tCoord a1, tCoord a2, tCoord bmin, tCoord bmax)
        {
            return (a1 <= a2)
                ? (a1 <= bmax && a2 >= bmin)
                : (a2 <= bmax && a1 >= bmin);
        };

        tCoord k1 = u1.dot(link);
        tCoord k2 = u1.dot(b2 - a1);
        return ranges_overlap(k1, k2, 0, u1.norm2());
    }
    else // secant
    {
        tCoord det2 = det*det;
        tCoord k1 = link.cross(u1) * det;
        if (k1 < 0 || k1 > det2) return false;
        tCoord k2 = link.cross(u2) * det;
        return k2 >= 0 && k2 <= det2;
    }
}

// ============================================================================
// partitions of a snake into segments
// ============================================================================
class sPartition {
    vector<int>segment;

public:
    sPartition() {} // only used in vector inits

    // split a flat snake into straight segments
    sPartition(unsigned generator, unsigned num_joints) 
    {
        int segment_len = 1;
        for (size_t i = 0; i != num_joints; i++)
        {
            if (generator & 1)
            {
                segment.push_back(segment_len);
                segment_len = 1;
            }
            else
            {
                segment_len++;
            }
            generator >>= 1;
        }
        segment.push_back(segment_len);
    }

    // direct access to underlying array of segment lengths
    size_t size(void) const { return segment.size(); }
    int & operator[] (size_t i) { return segment[i]; }
    int operator[] (size_t i) const { return segment[i]; }
};

// ============================================================================
// compact storage of a configuration (64 bits)
// ============================================================================
struct sConfiguration {
    unsigned partition;
    unsigned folding;

    sConfiguration(unsigned partition, unsigned folding) : partition(partition), folding(folding) {}

    // textual representation
    string text(unsigned length)
    {
        ostringstream res;

        unsigned f = folding;
        unsigned p = partition;

        int segment_len = 1;
        int direction = 1;
        for (size_t i = 1; i != length; i++)
        {
            if (p & 1)
            {
                res << segment_len * direction << ',';
                direction = (f & 1) ? -1 : 1;
                segment_len = 1;
            }
            else segment_len++;

            p >>= 1;
            f >>= 1;
        }
        res << segment_len * direction;
        return res.str();
    }

    // for final sorting
    bool operator< (const sConfiguration& c) const
    {
        return (partition == c.partition) ? folding < c.folding : partition < c.partition;
    }

    // hash table setup
    struct Hasher {
        size_t operator() (const sConfiguration& c) const
        {
            return c.partition ^ c.folding;
        }
    };
    struct Equalizer {
        bool operator() (const sConfiguration& c1, const sConfiguration& c2) const
        {
            return c1.partition == c2.partition && c1.folding == c2.folding;
        }
    };
    typedef unordered_set<sConfiguration, Hasher, Equalizer> set;

};

// ============================================================================
// snake ragdoll 
// ============================================================================
class tSnakeDoll {
    vector<tPoint>point; // snake geometry. Head at (0,0) pointing right

    // allows to check for collision with the area swept by a rotating segment
    struct rotatedSegment {
        struct segment { tPoint a, b; };
        segment org, end;
        tArc    arc[3];
        bool extra_arc; // see if third arc is needed

        // empty constructor to avoid wasting time in vector initializations
        rotatedSegment(){}
        // copy constructor is mandatory for vectors *but* shall never be used, since we carefully pre-allocate vector memory
        rotatedSegment(const rotatedSegment &){ assert(!"rotatedSegment should never have been copy-constructed"); }

        // rotate a segment
        rotatedSegment(tPoint pivot, int rotation, tPoint o1, tPoint o2)
        {
            arc[0] = tArc(pivot, o1, rotation);
            arc[1] = tArc(pivot, o2, rotation);
            tPoint middle;
            extra_arc = closest_point_within(o1, o2, pivot, middle);
            if (extra_arc) arc[2] = tArc(pivot, middle, rotation);
            org = { o1, o2 };
            end = { o1.rotate(rotation, pivot), o2.rotate(rotation, pivot) };
        }

        // check if a segment intersects the area swept during rotation
        bool intersects(tPoint p1, tPoint p2) const
        {
            auto print_seg = [&](const segment & s) { tprintf("(%d,%d)(%d,%d) -> (%d,%d)(%d,%d)", p1.x, p1.y, p2.x, p2.y, s.a.x, s.a.y, s.b.x, s.b.y); };
            auto print_arc = [&](int a) { tprintf("(%d,%d)(%d,%d) -> %d (%d,%d)[%f,%f]", p1.x, p1.y, p2.x, p2.y, a, arc[a].c.x, arc[a].c.y, arc[a].middle_vector.x, arc[a].middle_vector.y); };

            if (p1 == org.a) return false; // pivot is the only point allowed to intersect
            if (inter_seg_seg(p1, p2, org.a, org.b)) { print_seg(org);  return true; }
            if (inter_seg_seg(p1, p2, end.a, end.b)) { print_seg(end);  return true; }
            if (inter_seg_arc(p1, p2, arc[0])) { print_arc(0);  return true; }
            if (inter_seg_arc(p1, p2, arc[1])) { print_arc(1);  return true; }
            if (extra_arc && inter_seg_arc(p1, p2, arc[2])) { print_arc(2);  return true; }
            return false;
        }
    };

public:
    unsigned folding; // compressed folding list
    int level; // ;!;

    // holds results of a folding attempt
    class snakeFolding {
        friend class tSnakeDoll;
        vector<rotatedSegment>segment; // rotated segments
        unsigned joint;
        int direction;
        size_t i_rotate;

        // pre-allocate rotated segments
        void reserve(size_t length)
        {
            segment.clear(); // this supposedly does not release vector storage memory
            segment.reserve(length);
        }

        // handle one segment rotation
        void rotate(tPoint pivot, int rotation, tPoint o1, tPoint o2)
        {
            segment.emplace_back(pivot, rotation, o1, o2);
        }
    public:
        // nothing done during construction
        snakeFolding() {};
    };

    // empty default constructor to avoid wasting time in array/vector inits
    tSnakeDoll() {}

    // constructs ragdoll from compressed configuration
    tSnakeDoll(unsigned size, unsigned generator, unsigned folding) : point(size), folding(folding)
    {
        tPoint direction(1, 0);
        tPoint current = { 0, 0 };
        size_t p = 0;
        point[p++] = current;
        for (size_t i = 1; i != size; i++)
        {
            current = current + direction;
            if (generator & 1)
            {
                direction.rotate((folding & 1) ? -1 : 1);
                point[p++] = current;
            }
            folding >>= 1;
            generator >>= 1;
        }
        point[p++] = current;
        point.resize(p);
    }

    // constructs the initial flat snake
    tSnakeDoll(int size) : point(2), folding(0)
    {
        point[0] = { 0, 0 };
        point[1] = { size, 0 };
        level = 0;
    }

    // constructs a new folding with one added rotation
    tSnakeDoll(const tSnakeDoll & start, const snakeFolding & fold)
    {
        point.resize(start.point.size()+1);
        size_t i;

        // copy unmoved points
        for (i = 0; i != fold.i_rotate; i++) point[i] = start.point[i];

        // copy rotated points
        for (; i != start.point.size(); i++) point[i] = fold.segment[i - fold.i_rotate].end.a;
        point[i] = fold.segment[i - 1 - fold.i_rotate].end.b;

        // update folding bitfield
        folding = start.folding;
        if (fold.direction == -1) folding |= (1 << fold.joint);
        level = start.level + 1;
//tprintf("snake doll level %d fold %X\n", level, folding);
    }

    // prepare one folding at a specified joint
    // rotation is 1 for right, -1 for left
    bool fold_at(unsigned joint, int rotation, snakeFolding & res) const
    {
        // first bend is only on the right
        if (level == 0 && rotation == -1)
        {
            tprintf("1st left ");
            return false;
        }

        res.joint = joint;
        res.direction = rotation;

        // locate folding point
        joint++;
        tPoint pivot;
        size_t i_rotate = 0;
        for (size_t i = 1 ; i != point.size(); i++)
        {
            unsigned len = point[i].manhattan_distance(point[i-1]);
            if (len > joint)
            {
                pivot = point[i-1] + ((point[i] - point[i - 1]) / len) * joint;
                i_rotate = i;
                break;
            }
            else joint -= len;
        }
        res.i_rotate = i_rotate;

        // rotate around joint
        res.reserve(point.size() - i_rotate); // number of rotated segments
        res.rotate(pivot, rotation, pivot, point[i_rotate]);
        for (size_t i = i_rotate + 1; i != point.size(); i++) res.rotate (pivot, rotation, point[i-1], point[i]);

        // check collisions
        for (const rotatedSegment & fold : res.segment)
        {
            for (size_t i = 1; i != i_rotate; i++) if (fold.intersects(point[i], point[i - 1])) return false;
            if (fold.intersects(pivot, point[i_rotate - 1])) return false;
        }
        return true;
    }
};

// ============================================================================
// snake twisting engine
// ============================================================================
class cSnakeFolder {
    int length;
    unsigned num_joints;
    unsigned reverse_start;

    map<unsigned,sPartition>partitions;
    sConfiguration::set config_hash;

    // recursive folding
    void fold(unsigned generator, tSnakeDoll snake, unsigned first_joint)
    {
        // filter redundant partitions
        if (partitions.count(generator) == 0)
        {
            tprintf("%s %s P cut\n", string(snake.level, '|').c_str(), sConfiguration(generator, snake.folding).text(length).c_str());
            return;
        }

        // filter redundant foldings
        unsigned reverse = bit_reverse(snake.folding, num_joints);
        if (reverse & 1) reverse = ~reverse & ((1<<num_joints)-1); // invert only significant bits
        if (config_hash.count({ generator, reverse }))
        {
            tprintf("%s %s F cut\n", string(snake.level, '|').c_str(), sConfiguration(generator, snake.folding).text(length).c_str());
            return;
        }

        // store this configuration
        config_hash.emplace(generator, snake.folding);

        // try to bend remaining joints
        for (size_t joint = first_joint; joint != num_joints; joint++)
        {
            unsigned next_partition = generator | (1 << joint);
            tSnakeDoll::snakeFolding bend;

            tprintf("%s %s -> %s ", string(snake.level, '|').c_str(), sConfiguration(generator, snake.folding).text(length).c_str(), sConfiguration(next_partition, snake.folding).text(length).c_str());
            if (snake.fold_at(joint, 1, bend))
            {
                tprintf("ok\n");
                fold(next_partition, tSnakeDoll(snake, bend), joint + 1);
            }
            else tprintf("failed\n");

            tprintf("%s %s -> %s ", string(snake.level, '|').c_str(), sConfiguration(generator, snake.folding).text(length).c_str(), sConfiguration(next_partition, snake.folding | (1 << joint)).text(length).c_str());
            if (snake.fold_at(joint, -1, bend))
            {
                tprintf("ok\n");
                fold(next_partition, tSnakeDoll(snake, bend), joint + 1);
            }
            else tprintf("failed\n");
        }
    }

public:
    // all found configurations, in compressed format
    vector<sConfiguration>configurations;

    // constructor does all the job
    cSnakeFolder(int n) : length(n)
    {
        num_joints = length - 1;
        reverse_start = 1 << num_joints;

        // generate snake partitions
        unsigned num_part = 1 << num_joints;
        for (unsigned partition = 0; partition != num_part; partition++)
        {
            // filter out symetric partitions
            if (partitions.count(bit_reverse(partition, num_joints))) continue;

            // generate and store partition
            partitions[partition] = sPartition (partition, num_joints);
        }

        // launch recursive folding
        fold(0, tSnakeDoll(length), 0);

        // collect configurations
        for (auto c : config_hash) configurations.emplace_back(c);
        sort(configurations.begin(), configurations.end());
        config_hash.clear(); // that's a huge lump of memory taken care of
    }

    // get a ragdoll representing a particular configuration
    /*
       For a snake of length N, the ragdoll is a list of N+1 points defining the N unitary segments.
       By convention, the snake head starts at coordinates (0,0) pointing to the right.
    */
    tSnakeDoll doll(size_t conf)
    {
        if (conf >= configurations.size()) conf = 0; // out of bounds queries will get a flat snake
        sConfiguration & c = configurations[conf];
        return tSnakeDoll(length, c.partition, c.folding);
    }

    // get text description of a particular configuration
    /* 
       Absolute values are segment lengths
       Sign indicate a preceding turn (by convention, positive for right, negative for left)
       1st segment is always positive by convention.
       Snake length is the sum of segment lengths absolute values.

           ___   For instance 2,-1,3 represents a snake of length 6 bent as
        __|      2 segments, left turn, 1 segment, right turn, 3 segments
    */
    string text(size_t conf)
    {
        if (conf >= configurations.size()) return "**out of bounds**";
        return configurations[conf].text(length);
    }
};

// ============================================================================
// here we go
// ============================================================================
int main(int argc, char * argv[])
{
#ifdef NDEBUG
    if (argc != 2) panic("give me a snake length or else");
    int length = atoi(argv[1]);
#else
    (void)argc; (void)argv;
    int length = 5;
#endif // NDEBUG

    if (length <= 0 || length > MAX_LENGTH) panic("a snake of that length is hardly foldable");

    time_t start = time(NULL);
    cSnakeFolder snakes(length);
    time_t duration = time(NULL) - start;
#ifndef NDEBUG
    for (size_t i = 0; i != snakes.configurations.size(); i++) printf("%3d: %s\n", i, snakes.text(i).c_str());
#endif // NDEBUG
    printf ("Found %d configuration%c of length %d in %lds\n", snakes.configurations.size(), (snakes.configurations.size() == 1) ? '\0' : 's', length, duration);
    return 0;
}

Building the executable

Compile with g++ -O3 -std=c++11
I use MinGW under Win7 with g++4.8 for "linux" builds, so portabiity is not 100% guaranteed.

It also works (sort of) with a standard MSVC2013 project

Performances

It is my second computation-intensive challenge using STL new-ish hash tables (unordered_set), and again Microsoft implementation performs poorly, to say the least.

g++ build is 2.5 times faster and uses about 15% less memory.
On the other hand, I find Microsoft IDE a lot more convenient than Qt Creator, with better on-the-fly error checking and more powerful debug features.

The g++ build uses about 32 bytes/configuration.
A snake configuration is 8 bytes, the remaining 24 are mostly used by hash table internals.
For convenience I transfer the results from the hash table into a sorted vector in the end, which increases memory consumption significantly.
Even so, the program can compute up to n=19 before hitting the Win32 2Gb allocation limit, and compiling for x64 would allow to go a couple of steps beyond.

Since collision check is roughly in O(n), computation time should be in O(nkn), with k slightly lower than 3.
On my [email protected], n=19 takes about 4 minutes.

This means memory should be less an issue than speed.

Counting shapes

A N size snake has N-1 joints.
Each joint can be left straight or bent to the left or right (3 possibilities).
The number of possible foldings is thus 3N-1

However, many such foldings lead to identical shapes.

two shapes are identical if there is either a rotation or a symetry that can transform one into the other.

Let's define a segment as any straight part of the folded body.
For instance a size 5 snake folded at 2nd joint would have 2 segments (one 2 units long and the second 3 units long).
The first segment will be named head, and the last tail.

By convention we orient the snakes's head horizontally with the body pointing right (like in the OP first figure).

We designate a given figure with a list of signed segment lengths, with positive lengths indicating a right folding and negative ones a left folding.
Initial lenght is positive by convention.

Separating segments and bends

If we consider only the different ways a snake of length N can be split into segments, we end up with a repartition identical to the compositions of N.

Using the same algorithm as shown in the wiki page, it is easy to generate all the 2N-1 possible partitions of the snake.

Each partition will in turn generate all possible foldings by applying either left or right bends to all of its joints. One such folding will be called a configuration.

All possible partitions can be represented by an integer of N-1 bits, where each bit represents the presence of a joint. We will call this integer a generator.

Pruning partitions

By noticing that bending a given partition from the head down is equivalent to bending the symetrical partition from the tail up, we can find all couples of symetrical partitions and eliminate one out of two.
The generator of a symetrical partition is the partition's generator written in reverse bit order, which is trivially easy and cheap to detect.

This will eliminate almost half of the possible partitions, the exceptions being the partitions with "palindromic" generators that are left unchanged by bit reversal (for instance 00100100).

Taking care of horizontal symetries

With our conventions (a snake starts pointing to the right), the very first bend applied to the right will produce a family of foldings that will be horizontal symetrics from the ones that differ only by the first bend.

If we decide that the first bend will always be to the right, we eliminate all horizontal symetrics in one big swoop.

Mopping up the palindromes

These two cuts are efficient, but not enough to take care of these pesky palindromes.
The most thorough check in the general case is as follows:

consider a configuration C with a palindromic partition.

  • if we invert every bend in C, we end up with a horizontal symetric of C.
  • if we reverse C (applying bends from the tail up), we get the same figure rotated right
  • if we both reverse and invert C, we get the same figure rotated left.

We could check every new configuration against the 3 others. However, since we already generate only configurations starting with a right turn, we only have one possible symetry to check:

  • the inverted C will start with a left turn, which is by construction impossible to duplicate
  • out of the reversed and inverted-reversed configurations, only one will start with a right turn.
    That is the only one configuration we can possibly duplicate.

Order of generation

Clearly collision check will be the most time-consuming part, so the main avenue for optimization is to reduce these computations.

A possible solution is to have a "ragdoll snake" that will start in a flat configuration and be gradually bent, to avoid recomputing the whole snake geometry for each possible configuration.

By choosing the order in which configurations are tested, so that at most a ragdoll is stored for each total number of joints, we can limit the number of instances to N-1.

Collision check

When a snake folds around one joint, each rotated segment will sweep an area whose shape is anything but trivial.
Clearly you can check collisions by testing inclusion within all such swept areas individually, though a more global check would be more efficient.

After an interesting discussion with trichoplax and a bit of JavaScript to get my bearings, I devised a method that should enable to get an analytic description of the swept area under certain conditions.

To try to put it in a few words, if you call

  • C the center of rotation,
  • S a rotating segment of arbitrary lenght and direction that does not contain C,
  • L the line prolongating S
  • H the line orthogonal to L passing through C,
  • I the intersection of L and H,

maths http://petiteleve.free.fr/SO/sweep.png

For any segment that does not contain I, the swept area is bound by 2 arcs and 2 segments.
If I falls within the segment, a third arc must be taken into account.

This means we can check each unmoving segment against each rotating segment with

  • 2 segment-with-segment intersections
  • 2 or 3 segment-with-arc intersections

I used vector geometry to avoid trigonometric functions altogether.
Vector operations produce compact and (relatively) readable code.
Better than most of the matrix crunching stuff I found on MathWorld or many gaming sites, anyway.

This allows to compute segment-with-segment intersections with integer values.

Segment-to-arc intersection requires a floating point vector, but the logic should be immune to rounding errors.
I found this elegant and efficient solution in an obscure forum post. I wonder why it is not more widely publicized.

Why does it not work?

I am not done tweaking the collision code yet, but it does not seem to explain why a very few configurations are not detected.
For n=4 I miss one snake though collision detection works as planned (eliminating only the "square" snake biting its own tail).

Most likely the cuts are just a tad bit too selective, but for now I am not able to pinpoint the logic error.

user16991