Draw a random hexa-glyph

23

3

enter image description here

The above image is called a hexa-glyph. Hexa-glyphs are some cool patterns I made up while doodling during my DiffEq class. Here's how you make one:

  1. Consider the following set of points, shaped like a regular hexagram. The inner hexagon is what will contain the final glyph, while the outer 6 points form a star and are where we will start drawing our lines.

enter image description here

  1. From the outer six points, randomly select a pair. For efficiency, there should be at least one other point between the two selected points (otherwise, it would have no effect on the final figure). Then, from each of the two points, cast a ray towards the other. This ray is blocked by previous lines.

enter image description here

  1. Repeat this process until all 9 edges have been formed, as shown in the next few images.

enter image description here

  1. Here is an example of rays being blocked. The ends of the ray segment are still visible, but the middle portion is occluded by the first two segments we drew.

enter image description here

  1. These two rays are also "blocked," but this doesn't cause any visible difference because they are blocked by the same other line.

enter image description here

  1. Fast-forwarding until all 9 lines are drawn. If you want a more detailed explanation of these skipped steps, I can expound.

enter image description here

  1. Finally, remove the points of the star. To make it look prettier, the thick dots are also removed.

enter image description here

The challenge

You challenge is to output a visual representation of a random hexa-glyph. This is code-golf, fewest bytes wins.

  1. All possible hexa-glyphs should appear with some positive probability. Different hexa-glyphs are generated by changing the order in which the 9 edges are drawn.

  2. Furthermore, all images output by your program must be valid hexa-glyphs. Certain patterns (like a complete outline of the inner hexagon) cannot possibly appear as a hexa-glyph, and so you program mustn't output these.

  3. The output should be a graphical image (printed to screen or file).

  4. The hexagon must be regular, but can appear in any orientation.

  5. Reflections / rotations are not considered unique. (This might make requirement 1 easier to follow).

PhiNotPi

Posted 2016-04-02T19:29:28.210

Reputation: 26 739

8I made up while doodling during my DiffEq class. The way all great discoveries happen... :P – Rɪᴋᴇʀ – 2016-04-02T23:09:43.530

What are the minimal requirements for the image? To which extent must an ASCII art be recognizable as long as each edge is represented and placed in vaguely the right spot? – John Dvorak – 2016-04-03T01:18:14.163

@JanDvorak I removed the ASCII art option of the challenge (like within 2 minutes of posting) because programs that produce ASCII-art and graphical outputs aren't easily comparable. – PhiNotPi – 2016-04-03T01:21:13.427

what about pixel art then? A PPM header isn't too heavy, and afterwards the only difference is using '01' with space interleaved instead of ' *'. – John Dvorak – 2016-04-03T01:21:53.120

@JanDvorak Output would then be a properly formatted image file, right? Then I don't see anything wrong with it. – PhiNotPi – 2016-04-03T23:56:51.467

This reminds me of glyph hacking - Ingress, anyone?

– Hexaholic – 2016-04-04T07:14:33.097

My question is, how accurate do I need to be when placing the "lines"? I guess I can't just dump 18 pixels randomly to the top left corner, but can I dump 18 pixels in the approximately right spot and assume the user can read it already? – John Dvorak – 2016-04-04T07:16:20.530

@JanDvorak Sorry for my late reply. I'm going to say, that yes, you can form a line by placing 18 pixels in approximately the right spot. – PhiNotPi – 2016-04-06T13:03:27.337

Answers

18

Mathematica, 273 268 264 242 bytes

c=CirclePoints;b@_=k=1>0;Graphics[Line/@Cases[Append[Join@@({c@6,{3^.5/2,-Pi/6}~c~6}),{0,0}][[b@#=!k;#]]&/@TakeWhile[#,t=k;(r=t;t=b@#;r)&]&/@Join@@RandomSample[{#,Reverse@#}&/@Partition[Range@12,3,2,1]~Join~Array[{2#,13,2#+6}&,3]],{_,__}]]

The renders as a superscript T in Mathematica and is a postfix transpose operator.

Sorting out the bugs in this took forever... towards the end I hacked a few things together to make it work, so this is definitely suboptimal. I also wonder whether it might be better overall to implement the spec more literally via the lines across the outer hexagon and let Mathematica's geometry functions handle the intersections.

Note that this is a full program and if you want to run the code multiple times within a single REPL session you'll have to prefix it with Clear[b].

Here are the results of 20 runs:

enter image description here

Explanation

This solution doesn't make use of the outer star points at all. Instead it works directly with the points that are part of the hexaglyph and lines that cover three of them at a time.

Let's label the points:

enter image description here

1 starts on a slightly weird corner but this is due to the (also somewhat weird) default behaviour of CirclePoints. Starting the hexagon from there turned out cheapest.

Now we want to find the relevant lines through three of those points which correspond to connected points of the outer star. The ones around the hexagon are of course just 3 adjacent points (modulo 12), starting from an odd number. The ones across the centre consist of an even number n, 13 and n+6.

Representations of these lines (in the form of lists of three points are generated by the following code):

Partition[Range@12,3,2,1]~Join~Array[{2#,13,2#+6}&,3]

The Partition generates the lines around the hexagon and the Array the lines through the centre. To process both beams, we map this function over the list of lines:

{#,Reverse@#}&

Now we shuffle these with RandomSample to process them in a random order. Join @@ flattens the list of pairs so that we have a list of beams.

Short intermission: to keep track of which points are already blocked we use a lookup function b, which is initialised to True for all values by b@_=k=1>0;. When processing a beam we keep all points until the first point that has b[n] == False (including that one):

TakeWhile[#,t=k;(r=t;t=b@#;r)&]&

I feel like this is the most golfable part right now... the use of two temporary variables to play Mastermind seems really expensive. Anyway, the result of this gives us the points in a line we're allowed to draw. Now this function is mapped over each of those points:

Append[Join@@({c@6,{3^.5/2,-Pi/6}~c~6}),{0,0}][[b@#=!k;#]]&

The first part generates the list of all 13 points using the interleaved results of two calls to CirclePoints (with different radii for the edge centres and the corners of the hexagon). Note the b@#=!k which now sets the lookup table's value for the current point to False so that no further beam can pass through it. Finally, the value is used as an index into the list of coordinates to get the correct 2D point.

Cases[...,{_,__}]

This discards all single-element lists, because they would render as individual (and visible) points. Finally we render the result:

Graphics[Line/@...]

Martin Ender

Posted 2016-04-02T19:29:28.210

Reputation: 184 808

b@_=1>0=b=1>0& – CalculatorFeline – 2016-04-03T03:22:44.543

@CatsAreFluffy I don't think that works, because I need to be able to overwrite individual values later on. – Martin Ender – 2016-04-03T07:17:21.440

Nice use of CirclePoints. – DavidC – 2016-04-03T12:15:54.517

I appreciated that Youtube link. – DanTheMan – 2016-08-29T05:00:49.790

8

Shoes (Ruby) Rev C 184 bytes

12 bytes saved by transferring responsibility for checking if a particular half-line should be drawn from the main program to the drawing method. The main program still has to check if the whole line is completely blocked, though.

Shoes.app{t=[]
d=->p,q{t[p]&&t[q]||line(p/6*8,p%6*14,q/6*8,q%6*14)}
%w{1I IW WM M5 5' '1 =A P. R,}.shuffle.map{|i|b=i.sum/2
c=b*2-a=i.ord
t[a]&&t[c]||(d[a,b]
d[b,c]
t[a]=t[b]=t[c]=1)}}

Shoes (Ruby) 205 ... Rev B 196 bytes

Shoes is a ruby-based tool for building GUI's etc. It's the first time I've used it. mothereff.in/byte-counter counts my submission as 196 bytes, but for some reason Shoes counts it as 202.

Also, Ruby lets you do things like t[a=i.ord] but strangely, it seems not to work as expected with Shoes.

Shoes.app{t=[]
d=->p,q{line(p/6*8,p%6*14,q/6*8,q%6*14)}
%w{1I IW WM M5 5' '1 =A P. R,}.shuffle.map{|i|b=i.sum/2
c=b*2-a=i.ord
t[a]&&t[c]||(t[a]&&t[b]||d[a,b]
t[b]&&t[c]||d[b,c]
t[a]=t[b]=t[c]=1)}}

Explanation

I don't consider the parts of the line outside the hexagon. I only draw the part that needs to be drawn. The important thing is whether the lines cross the intersections (If we only draw the parts that need to be drawn, this means they begin / end at the intersections.)

The basic rule is that if both endpoints of a line have been visited, the line is blocked and should not be drawn. As the lines are drawn in two halves, we also have to check if the midpoint has been visited to see if each half should be drawn or not.

I keep track of which points have been visited in the array t[]. This ends up containing an entry for each physical coordinate on the grid below. There is no separate 13-element logical array. By the end, t[] may have 87 elements, though only up to 13 will contain useful data.

Internally, the coordinates of the endpoints of the lines are given by a single number z, where z%6 is the y coordinate and z/6 is the x coordinate. In this system the hexagon is flattened. When the lines are plotted, the x scale is multiplied by 8 and the y scale is multiplied by 14, which is a very close rational approximation to the correct ratio: 14/8=1.75 vs sqrt(3)=1.732.

The internal coordinate system is shown below, with a few sample outputs.

enter image description here

Ungolfed

Shoes.app{
  t=[]                                          #Empty array for status tracking
  d=->p,q{line(p/6*8,p%6*14,q/6*8,q%6*14)}      #Drawing method. Convert p and q into x,y pairs, scale and draw line.
  %w{1I IW WM M5 5' '1 =A P. R,}.shuffle.map{|i|#take an array of the coordinates of the endpoints of each line, shuffle, then for each line
    b=i.sum/2                                   #b = midpoint of line, convert ASCII sum to number (average of the two coordinates)
    a=i.ord                                     #a = first endpoint of line, convert ASCII to number (no need to write i[0].ord)
    c=b*2-a                                     #c = second endpoint of line (calculating is shorter than writing i[1].ord)
    t[a]&&t[c]||(                               #if both endpoints have already been visited, line is completely blocked, do nothing. ELSE
      t[a]&&t[b]||d[a,b]                        #if first endpoint and midpoint have not both been visited, draw first half of line
      t[b]&&t[c]||d[b,c]                        #if second endpoint and midpoint have not both been visited, draw second half of line
      t[a]=t[b]=t[c]=1                          #mark all three points of the line as visited
    )
  }
}

More Sample outputs

These were done with an older version of the program. The only difference is that the positioning of the hexaglyph in the window is now slightly different.

enter image description here

Level River St

Posted 2016-04-02T19:29:28.210

Reputation: 22 049

mothereff.in/byte-counter counts my submission as 196 bytes, but for some reason Shoes counts it as 202. I don't 100% know if this is true, but I think that the reason that Shoes counted your code as 202 bytes instead of 196 is because your newlines are actually a two character sequence "\r\n." This makes every newline get counted twice. Here is a stack overflow answer regarding \r and \n. – K Zhang – 2016-07-08T19:52:39.083

Hehe I can't get over the name Ruby with Shoes XD – Beta Decay – 2016-08-29T10:50:55.090

3

Python, 604 591 574 561 538 531 536 534 528 493 483 452 431 420 419 415 388 385 384 bytes

I have adapted Level River St's idea of checking if a line will be blocked by checking if both endpoints of the line have already been visited before. This saves 27 bytes. Golfing suggestions welcome.

Edit: Bug fixing and golfing g(p,q) for 3 bytes. Golfed L for one byte.

from turtle import*
from random import*
R=range
G=goto
*L,=R(9)
shuffle(L)
a=[0]*13
ht()
T=12
c=[(j.imag,j.real)for j in(1j**(i/3)*T*.75**(i%2/2)for i in R(T))]+[(0,0)]
def g(p,q):pu();G(c[p]);a[p]*a[q]or pd();G(c[q])
for m in L:
 p=2*m;x,y,z=R(p,p+3)
 if m<6:
  if a[x]*a[z%T]<1:g(x,y);g(y,z%T);a[x]=a[y]=a[z%T]=1
 else:
  if a[p-11]*a[p-5]<1:g(p-11,T);g(p-5,T);a[p-11]=a[p-5]=a[T]=1

Ungolfing:

from turtle import*
from random import*

def draw_line(points, p_1, p_2):
    penup()
    goto(points[p_1])
    if not (a[p] and a[q]):
        pendown()
    goto(points[p_2])

def draw_glyph():
    ht()
    nine_lines = list(range(9))
    shuffle(nine_lines)
    size = 12
    center = [0,0]

    points = []
    for i in range(12):      # put in a point of a dodecagon
                             # if i is even, keep as hexagon point
                             # else, convert to hexagon midpoint
        d = 1j**(i/3) * 12   # dodecagon point
        if i%2:
            d *= .75**.5     # divide by sqrt(3/4) to get midpoint
        points += (d.imag, d.real)
    points.append(center)

    a = [0]*13
    for m in nine_lines:
        p = 2*m
        if m<6:
            x, y, z = p, p+1, p+2
            if not (a[x] and a[z%12]):
                draw_line(points, x, y)
                draw_line(points, y, z%12)
                a[x] = a[y] = a[z%12] = 1
        else:
            if not (a[p-11] and a[p-5]):
                draw_line(p-11, 12)
                draw_line(p-5, 12)
                a[p-11] = a[p-5] = a[12] = 1

The hexa-glyphs themselves are quite small as we use a 12-pixel hexagon as a base (for golfing reasons). Here are some example hexa-glyphs (apologies for the poor cropping):

An example hexa-glyph An example hexa-glyph An example hexa-glyph An example hexa-glyph An example hexa-glyph An example hexa-glyph

Sherlock9

Posted 2016-04-02T19:29:28.210

Reputation: 11 664

Could save a few bytes: R=range;G=goto – Tim Čas – 2016-08-28T11:26:21.910