Haskell, 166 154 bytes
(-12 bytes thanks to Laikoni, (zip and list comprehension instead of zipWith and lambda, better way of generating the first line))
i#n|let k!p=p:(k+1)![m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))|(l,m,r)<-zip3(1:p)p$tail p++[1]];x=1<$[2..2^n]=mapM(putStrLn.map("M "!!))$take(2^n)$1!(x++0:x)
Try it online!
Explanation:
The function i#n
draws an ASCII-Triangle of height 2^n
after i
steps of iteration.
The encoding used internally encodes empty positions as 1
and full positions as 0
. Therefore, the first line of the triangle is encoded as [1,1,1..0..1,1,1]
with 2^n-1
ones on both sides of the zero. To build this list, we start with the list x=1<$[2..2^n]
, i.e. the list [2..2^n]
with everything mapped to 1
. Then, we build the complete list as x++0:x
The operator k!p
(detailed explanation below), given a line index k
and a corresponding p
generates an infinite list of lines that follow p
. We invoke it with 1
and the starting line described above to get the entire triangle, and then only take the first 2^n
lines. Then, we simply print each line, replacing 1
with space and 0
with M
(by accessing the list "M "
at location 0
or 1
).
Operator k!p
is defined as follows:
k!p=p:(k+1)![m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))|(l,m,r)<-zip3(1:p)p$tail p++[1]]
First, we generate three versions of p
: 1:p
which is p
with a 1
prepended, p
itself and tail p++[1]
which is everything but the first element of p
, with a 1
appended. We then zip these three lists, giving us effectively all elements of p
with their left and right neighbors, as (l,m,r)
. We use a list comprehension to then calculate the corresponding value in the new line:
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
To understand this expression, we need to realise there are two basic cases to consider: Either we simply expand the previous line, or we are at a point where an empty spot in the triangle begins. In the first case, we have a filled spot if any of the spots in the neighborhood is filled. This can be calculated as m*l*r
; if any of these three is zero, then the new value is zero. The other case is a bit trickier. Here, we basically need edge detection. The following table gives the eight possible neighborhoods with the resulting value in the new line:
000 001 010 011 100 101 110 111
1 1 1 0 1 1 0 1
A straightforward formula to yield this table would be 1-m*r*(1-l)-m*l*(1-r)
which simplifies to m*(2*l*r-l-r)+1
. Now we need to choose between these two cases, which is where we use the line number k
. If mod k (2^(n-i)) == 0
, we have to use the second case, otherwise, we use the first case. The term 0^(mod k(2^n-i))
therefore is 0
if we have to use the first case and 1
if we have to use the second case. As a result, we can use
m*l*r+(m*(l*r-l-r)+1)*0^mod k(2^(n-i))
in total - if we use the first case, we simply get m*l*r
, while in the second case, an additional term is added, giving the grand total of m*(2*l*r-l-r)+1
.