C ++ 11 - fonctionne presque :)
Après avoir lu cet article , j'ai recueilli des morceaux de sagesse de ce gars qui a apparemment travaillé pendant 25 ans sur le problème moins compliqué de compter les chemins d'auto-évitement sur un réseau carré.
#include <cassert>
#include <ctime>
#include <sstream>
#include <vector>
#include <algorithm> // sort
using namespace std;
// theroretical max snake lenght (the code would need a few decades to process that value)
#define MAX_LENGTH ((int)(1+8*sizeof(unsigned)))
#ifndef _MSC_VER
#ifndef QT_DEBUG // using Qt IDE for g++ builds
#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 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;
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); // works only for +-90° rotations. The vector should be normalized to circle radius in the general case
middle_vector_norm2 = r.norm2();
dp_limit = ((tFloatPoint)r).dot(middle_vector);
assert (middle_vector == tPoint(0, 0) || dp_limit != 0);
}
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;
tPoint disp = (p1p2 * p1c.dot(p1p2)) / p1p2.norm2();
return p1 + disp;
}
// 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);
}
// ============================================================================
// compact storage of a configuration (64 bits)
// ============================================================================
struct sConfiguration {
unsigned partition;
unsigned folding;
explicit sConfiguration() {}
sConfiguration(unsigned partition, unsigned folding) : partition(partition), folding(folding) {}
// add a bend
sConfiguration bend(unsigned joint, int rotation) const
{
sConfiguration res;
unsigned joint_mask = 1 << joint;
res.partition = partition | joint_mask;
res.folding = folding;
if (rotation == -1) res.folding |= joint_mask;
return res;
}
// textual representation
string text(unsigned length) const
{
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;
}
};
// ============================================================================
// static snake geometry checking grid
// ============================================================================
typedef unsigned tConfId;
class tGrid {
vector<tConfId>point;
tConfId current;
size_t snake_len;
int min_x, max_x, min_y, max_y;
size_t x_size, y_size;
size_t raw_index(const tPoint& p) { bound_check(p); return (p.x - min_x) + (p.y - min_y) * x_size; }
void bound_check(const tPoint& p) const { assert(p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y); }
void set(const tPoint& p)
{
point[raw_index(p)] = current;
}
bool check(const tPoint& p)
{
if (point[raw_index(p)] == current) return false;
set(p);
return true;
}
public:
tGrid(int len) : current(-1), snake_len(len)
{
min_x = -max(len - 3, 0);
max_x = max(len - 0, 0);
min_y = -max(len - 1, 0);
max_y = max(len - 4, 0);
x_size = max_x - min_x + 1;
y_size = max_y - min_y + 1;
point.assign(x_size * y_size, current);
}
bool check(sConfiguration c)
{
current++;
tPoint d(1, 0);
tPoint p(0, 0);
set(p);
for (size_t i = 1; i != snake_len; i++)
{
p = p + d;
if (!check(p)) return false;
if (c.partition & 1) d = d.rotate((c.folding & 1) ? -1 : 1);
c.folding >>= 1;
c.partition >>= 1;
}
return check(p + d);
}
};
// ============================================================================
// 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; };
tPoint org;
segment 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;
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_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) return false; // pivot is the only point allowed to intersect
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:
sConfiguration configuration;
bool valid;
// 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(unsigned size)
{
segment.reserve (size);
}
};
// 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), configuration(generator,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), configuration(0,0), valid(true)
{
point[0] = { 0, 0 };
point[1] = { size, 0 };
}
// constructs a new folding with one added rotation
tSnakeDoll(const tSnakeDoll & parent, unsigned joint, int rotation, tGrid& grid)
{
// update configuration
configuration = parent.configuration.bend(joint, rotation);
// locate folding point
unsigned p_joint = joint+1;
tPoint pivot;
size_t i_rotate = 0;
for (size_t i = 1; i != parent.point.size(); i++)
{
unsigned len = parent.point[i].manhattan_distance(parent.point[i - 1]);
if (len > p_joint)
{
pivot = parent.point[i - 1] + ((parent.point[i] - parent.point[i - 1]) / len) * p_joint;
i_rotate = i;
break;
}
else p_joint -= len;
}
// rotate around joint
snakeFolding fold (parent.point.size() - i_rotate);
fold.rotate(pivot, rotation, pivot, parent.point[i_rotate]);
for (size_t i = i_rotate + 1; i != parent.point.size(); i++) fold.rotate(pivot, rotation, parent.point[i - 1], parent.point[i]);
// copy unmoved points
point.resize(parent.point.size()+1);
size_t i;
for (i = 0; i != i_rotate; i++) point[i] = parent.point[i];
// copy rotated points
for (; i != parent.point.size(); i++) point[i] = fold.segment[i - i_rotate].end.a;
point[i] = fold.segment[i - 1 - i_rotate].end.b;
// static configuration check
valid = grid.check (configuration);
// check collisions with swept arcs
if (valid && parent.valid) // ;!; parent.valid test is temporary
{
for (const rotatedSegment & s : fold.segment)
for (size_t i = 0; i != i_rotate; i++)
{
if (s.intersects(point[i+1], point[i]))
{
//printf("! %s => %s\n", parent.trace().c_str(), trace().c_str());//;!;
valid = false;
break;
}
}
}
}
// trace
string trace(void) const
{
size_t len = 0;
for (size_t i = 1; i != point.size(); i++) len += point[i - 1].manhattan_distance(point[i]);
return configuration.text(len);
}
};
// ============================================================================
// snake twisting engine
// ============================================================================
class cSnakeFolder {
int length;
unsigned num_joints;
tGrid grid;
// filter redundant configurations
bool is_unique (sConfiguration c)
{
unsigned reverse_p = bit_reverse(c.partition, num_joints);
if (reverse_p < c.partition)
{
tprintf("P cut %s\n", c.text(length).c_str());
return false;
}
else if (reverse_p == c.partition) // filter redundant foldings
{
unsigned first_joint_mask = c.partition & (-c.partition); // insulates leftmost bit
unsigned reverse_f = bit_reverse(c.folding, num_joints);
if (reverse_f & first_joint_mask) reverse_f = ~reverse_f & c.partition;
if (reverse_f > c.folding)
{
tprintf("F cut %s\n", c.text(length).c_str());
return false;
}
}
return true;
}
// recursive folding
void fold(tSnakeDoll snake, unsigned first_joint)
{
// count unique configurations
if (snake.valid && is_unique(snake.configuration)) num_configurations++;
// try to bend remaining joints
for (size_t joint = first_joint; joint != num_joints; joint++)
{
// right bend
tprintf("%s -> %s\n", snake.configuration.text(length).c_str(), snake.configuration.bend(joint,1).text(length).c_str());
fold(tSnakeDoll(snake, joint, 1, grid), joint + 1);
// left bend, except for the first joint
if (snake.configuration.partition != 0)
{
tprintf("%s -> %s\n", snake.configuration.text(length).c_str(), snake.configuration.bend(joint, -1).text(length).c_str());
fold(tSnakeDoll(snake, joint, -1, grid), joint + 1);
}
}
}
public:
// count of found configurations
unsigned num_configurations;
// constructor does all the work :)
cSnakeFolder(int n) : length(n), grid(n), num_configurations(0)
{
num_joints = length - 1;
// launch recursive folding
fold(tSnakeDoll(length), 0);
}
};
// ============================================================================
// 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 = 12;
#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;
printf ("Found %d configuration%c of length %d in %lds\n", snakes.num_configurations, (snakes.num_configurations == 1) ? '\0' : 's', length, duration);
return 0;
}
Construire l'exécutable
Compiler avec
J'utilise MinGW sous Win7 avec g ++ 4.8 pour les builds "linux", donc la portabilité n'est pas garantie à 100%.g++ -O3 -std=c++11
Il fonctionne également (en quelque sorte) avec un projet MSVC2013 standard
En définissant NDEBUG
, vous obtenez des traces d'exécution de l'algorithme et un résumé des configurations trouvées.
Les performances
avec ou sans tables de hachage, le compilateur Microsoft fonctionne misérablement: la construction g ++ est 3 fois plus rapide .
L'algorithme n'utilise pratiquement pas de mémoire.
Étant donné que le contrôle de collision est à peu près en O (n), le temps de calcul doit être en O (nk n ), avec k légèrement inférieur à 3.
Sur mon i3-2100@3.1GHz, n = 17 prend environ 1:30 (environ 2 millions serpents / minute).
Je n'ai pas fini d'optimiser, mais je ne m'attendrais pas à plus d'un gain x3, donc en gros je peux espérer atteindre peut-être n = 20 en moins d'une heure, ou n = 24 en moins d'une journée.
Atteindre la première forme inflexible connue (n = 31) prendrait entre quelques années et une décennie, en supposant qu'aucune panne de courant.
Compter les formes
A N serpent de taille a N-1 articulations.
Chaque articulation peut être laissée droite ou pliée à gauche ou à droite (3 possibilités).
Le nombre de pliages possibles est donc de 3 N-1 .
Les collisions réduiront quelque peu ce nombre, de sorte que le nombre réel est proche de 2,7 N-1
Cependant, de nombreux pliages de ce type conduisent à des formes identiques.
deux formes sont identiques s'il y a soit une rotation soit une symétrie qui peuvent se transformer l'une en l'autre.
Définissons un segment comme n'importe quelle partie droite du corps plié.
Par exemple, un serpent de taille 5 plié à la 2e articulation aurait 2 segments (un de 2 unités de long et le second de 3 unités de long).
Le premier segment sera nommé tête et la dernière queue .
Par convention, nous orientons la tête du serpent horizontalement avec le corps pointé vers la droite (comme dans la première figure OP).
Nous désignons une figure donnée avec une liste de longueurs de segments signées, avec des longueurs positives indiquant un pliage à droite et des négatives un pliage à gauche.
La longueur initiale est positive par convention.
Séparation des segments et des coudes
Si nous considérons uniquement les différentes façons dont un serpent de longueur N peut être divisé en segments, nous nous retrouvons avec une répartition identique aux compositions de N.
En utilisant le même algorithme que celui montré sur la page wiki, il est facile de générer toutes les 2 N-1 partitions possibles du serpent.
Chaque cloison générera à son tour tous les plis possibles en appliquant des plis à gauche ou à droite à tous ses joints. Un tel pliage sera appelé configuration .
Toutes les partitions possibles peuvent être représentées par un entier de N-1 bits, où chaque bit représente la présence d'un joint. Nous appellerons cet entier un générateur .
Élagage des partitions
En remarquant que plier une partition donnée de la tête vers le bas équivaut à plier la partition symétrique de la queue vers le haut, nous pouvons trouver tous les couples de partitions symétriques et éliminer une sur deux.
Le générateur d'une partition symétrique est le générateur de la partition écrit dans l'ordre inverse des bits, ce qui est trivialement facile et bon marché à détecter.
Cela éliminera près de la moitié des partitions possibles, les exceptions étant les partitions avec des générateurs "palindromiques" qui restent inchangés par inversion de bits (par exemple 00100100).
Prendre soin des symétries horizontales
Avec nos conventions (un serpent commence à pointer vers la droite), le tout premier pli appliqué à droite produira une famille de pliages qui seront symétriques horizontaux de ceux qui ne diffèrent que par le premier pli.
Si nous décidons que le premier virage sera toujours vers la droite, nous éliminons toutes les symétries horizontales d'un seul coup.
Nettoyer les palindromes
Ces deux coupes sont efficaces, mais pas suffisantes pour prendre soin de ces palindromes embêtants.
La vérification la plus approfondie dans le cas général est la suivante:
considérons une configuration C avec une partition palindromique.
- si nous inversons chaque virage en C, nous nous retrouvons avec une symétrique horizontale de C.
- si nous inversons C (en appliquant des virages de la queue vers le haut), nous obtenons la même figure tournée vers la droite
- si nous inversons et inversons C tous les deux, nous obtenons la même figure tournée vers la gauche.
Nous pourrions vérifier chaque nouvelle configuration par rapport aux 3 autres. Cependant, comme nous ne générons déjà que des configurations commençant par un virage à droite, nous n'avons qu'une seule symétrie possible à vérifier:
- le C inversé commencera par un virage à gauche, ce qui est par construction impossible à reproduire
- sur les configurations inversée et inversée-inversée, une seule commencera par un virage à droite.
C'est la seule configuration que nous pouvons éventuellement dupliquer.
Éliminer les doublons sans stockage
Mon approche initiale était de stocker toutes les configurations dans une énorme table de hachage, pour éliminer les doublons en vérifiant la présence d'une configuration symétrique précédemment calculée.
Grâce à l'article précité, il est devenu clair que, comme les partitions et les pliages sont stockés sous forme de champs binaires, ils peuvent être comparés comme n'importe quelle valeur numérique.
Donc, pour éliminer un membre d'une paire symétrique, vous pouvez simplement comparer les deux éléments et garder systématiquement le plus petit (ou le plus grand, comme vous le souhaitez).
Ainsi, tester une configuration pour la duplication revient à calculer la partition symétrique, et si les deux sont identiques, le pliage. Aucune mémoire n'est nécessaire du tout.
Ordre de génération
Il est clair que la vérification des collisions sera la partie la plus longue, donc la réduction de ces calculs est un gain de temps majeur.
Une solution possible est d'avoir un "serpent ragdoll" qui commencera dans une configuration plate et sera progressivement plié, pour éviter de recalculer la géométrie entière du serpent pour chaque configuration possible.
En choisissant l'ordre dans lequel les configurations sont testées, de sorte qu'au plus un chiffon soit stocké pour chaque nombre total de joints, nous pouvons limiter le nombre d'instances à N-1.
J'utilise une analyse récursive du saké de la queue vers le bas, en ajoutant une seule articulation à chaque niveau. Ainsi, une nouvelle instance de ragdoll est construite au-dessus de la configuration parent, avec un seul virage supplémentaire.
Cela signifie que les plis sont appliqués dans un ordre séquentiel, ce qui semble être suffisant pour éviter les auto-collisions dans presque tous les cas.
Lorsque l'auto-collision est détectée, les virages qui conduisent au mouvement offensant sont appliqués dans tous les ordres possibles jusqu'à ce que le repli légitime soit trouvé ou que toutes les combinaisons soient épuisées.
Contrôle statique
Avant même de penser aux pièces mobiles, j'ai trouvé plus efficace de tester la forme finale statique d'un serpent pour les auto-intersections.
Cela se fait en dessinant le serpent sur une grille. Chaque point possible est tracé de la tête vers le bas. S'il y a une auto-intersection, au moins une paire de points tombera au même endroit. Cela nécessite exactement N tracés pour toute configuration de serpent, pour un temps O (N) constant.
Le principal avantage de cette approche est que le test statique à lui seul sélectionnera simplement des chemins valables auto-évitables sur un réseau carré, ce qui permet de tester l'ensemble de l'algorithme en inhibant la détection de collision dynamique et en nous assurant de trouver le nombre correct de ces chemins.
Contrôle dynamique
Lorsqu'un serpent se replie autour d'une articulation, chaque segment pivoté balaie une zone dont la forme est tout sauf anodine.
De toute évidence, vous pouvez vérifier les collisions en testant l'inclusion dans toutes ces zones balayées individuellement. Une vérification globale serait plus efficace, mais compte tenu de la complexité des zones, je ne peux penser à aucune (sauf peut-être à l'aide d'un GPU pour dessiner toutes les zones et effectuer une vérification globale).
Étant donné que le test statique prend en charge les positions de début et de fin de chaque segment, il nous suffit de vérifier les intersections avec les arcs balayés par chaque segment en rotation.
Après une discussion intéressante avec trichoplax et un peu de JavaScript pour me repérer, j'ai trouvé cette méthode:
Pour essayer de le dire en quelques mots, si vous appelez
- C le centre de rotation,
- S un segment tournant de longueur et de direction arbitraires qui ne contient pas C ,
- L la ligne prolongeant S
- H la ligne orthogonale à L passant par C ,
- I l'intersection de L et H ,
(source: free.fr )
Pour tout segment ne contenant pas de I , la zone balayée est délimitée par 2 arcs (et 2 segments déjà pris en charge par le contrôle statique).
Si je tombe dans le segment, l'arc balayé par I doit également être pris en compte.
Cela signifie que nous pouvons comparer chaque segment immobile contre chaque segment rotatif avec 2 ou 3 intersections segment-avec-arc
J'ai utilisé la géométrie vectorielle pour éviter complètement les fonctions trigonométriques.
Les opérations vectorielles produisent du code compact et (relativement) lisible.
L'intersection segment-arc nécessite un vecteur à virgule flottante, mais la logique doit être à l'abri des erreurs d'arrondi.
J'ai trouvé cette solution élégante et efficace dans un post obscur sur le forum. Je me demande pourquoi il n'est pas plus largement diffusé.
Est-ce que ça marche?
L'inhibition de la détection de collision dynamique produit le nombre de chemins auto-évitants corrects jusqu'à n = 19, donc je suis assez confiant que la disposition globale fonctionne.
La détection de collision dynamique produit des résultats cohérents, bien que la vérification des virages dans un ordre différent soit manquante (pour l'instant).
Par conséquent, le programme compte les serpents qui peuvent être courbés de la tête vers le bas (c'est-à-dire avec les articulations repliées par ordre croissant de distance de la tête).