Embedded Hexagons!

Charcoal, 40 29 bytes

11 bytes saved thanks to @Neil by changing the while loop to a for-loop amongst other tricks

FN«AX²ιβ×__β↓↘β←↙β↑←×__β↖β→↗β

Try it online!

Explanation (outdated)

This program starts with generating the largest hexagon, and then does the smaller ones one-by-one in a while loop (1-indexed). For reference, α is the input number, β is the variable that contains 2^(α-1) and ι is the iterating variable in the loop.

Nα                        # Take input and store in α
Wα«                       # While α do:
 ×_X²ι                    #  Write "_"*(2**ι); this forms the top edge of the hexagon
 ↓                         #  Go down
 AX²⁻ι¹β                 #  Assign 2**(ι-1) to β
 ↘β←                       #  Write \ β times in a SE direction (top right edge) and then go left
 ↙β↑                       #  Write / β times in a SW direction (bottom right edge) and then go up
 ←×_X²ι                   #  Write the bottom edge
 ↖β→↗β                    #  Just like before, write the top and bottom left edges
 A⁻α¹α                    #  Decrement α
                          # Now the pointer is at the top left corner of the hexagon,
                          # from where the other smaller hexagons will soon be generated

Haskell, 230 217 207 bytes

EDIT:

  • -13 bytes: @xnor saw that my # could be just max.
  • -10 bytes: And also that zipWith and p could be merged into a ? operator, and that I'd (somehow!) reimplemented replicate.

m takes an Integer and returns a String.

m n=unlines.foldr1 o$((2^n)&).(2^)<$>[0..n]
l&t|a<-c[l,2*t]" _",b<-[c[l-i,1,2*t+2*i-2,1,l-i]" / \\ "|i<-[1..t]]=a:b++r(r<$>o[a]b)
c=(concat.).z replicate
o=max?' '?""
f?e=z f.(++repeat e)
r=reverse
z=zipWith

Try it online!

How it works

  • m is the main function. It uses & to generate the hexagons with proper padding, then folds them together with o.
  • l&t generates a small hexagon of side length t, padded inside a large one of side length l, as a list of String lines.
    • a is the top line of the hexagon, with the underscores.
    • b is a list of the other lines in the upper half of the hexagon. The lines of b are centered in the padding, which is rectangular; this allows the next step to work.
    • The bottom half of the hexagon is a overlaid on top of b with o, then reversed (both order of lines and within each line).
  • c takes two arguments, a list of lengths and a string, and generates a string that has as many copies of each character in the original as the corresponding length, e.g. c[1,3,2]"abc" == "abbbcc". It is used in & to generate the lines.
  • o takes two arguments representing pictures as lists of lines, and overlays the first, smaller one on top of the second one.
    • It is used both to combine hexagons and to add the bottom to each hexagon.
    • It essentially works by using ? twice to pad the first picture with infinitely many spaces both downwards and rightwards, then zipping together corresponding characters with max, which selects the non-space character if there is one.
  • (f?e)l m pads a list l by appending infinitely many 'e' elements, then zips the resulting list and the list m with the f function.

JavaScript (ES6), 258 bytes

f=(n,s=` `.repeat(1<<n),p=(n?f(n-1):`


`).replace(/(.*)\n/g,s+`$1 `+s)+s,t=`_`.repeat(2<<n))=>(s+t+s+`
`+s.replace(/ /g,"$'/$'$'  $`$`$`$`\\$'\n")).replace(/ /g,(c,i)=>p[i+(1<<n>>1)])+s.replace(/ /g,"$`\\$`$`  $'$'$'$'/$`\n").replace(/ +\/( *)\n$/,t+`/$1
`)
<input type=number min=0 oninput=o.textContent=f(+this.value)><pre id=o>

Explanation: For hexagons after the first, the previous hexagon is first generated and padded on each side (this relies on the output being a rectangle). (For the first heading, some dummy padding is created.) The top and top sides of the hexagon are generated, and all the spaces merged with the previous hexagon. (There's some trickery in order to get the hexagons to line up; this would be easier if extra margins were allowed.) The bottom sides of the hexagon are generated analogously to the top sides, and the bottom of the hexagon is then filled in. Care has to be taken to return rectangular output, including a trailing newline, for the recursion to work.