text:
Translate English to Corpus
The Corpus are a fictional faction from the popular video game Warframe who have a somewhat interesting language.
Whilst other factions in the game such as the Grineer have some logic behind their language, the Corpus is simply a lossy substitution of the English language.
All Corpus words are the same as their English counterparts, except with the following alphabetical mappings:

This causes some problems with pronunciation as:
yes becomes yey
say becomes yay
yay becomes yay
sassy becomes yayyy
case becomes yaye
Here's a text version of the mappings:
a -> a
b -> t
c -> y
d -> p
e -> e
f -> t
g -> j
h -> k
i -> i
j -> t
k -> k
l -> p
m -> s
n -> t
o -> o
p -> k
q -> r
r -> t
s -> y
t -> p
u -> u
v -> t
w -> j
x -> k
y -> y
z -> b

The challenge
Given text using the English alphabet, output its Corpus translation.
For example, the text Hello, World! becomes Keppo, Jotpp! in Corpus
The Rules

Input will only consist of ASCII printable characters
Input text may contain whitespace and punctuation, these must be preserved
Capitalization of letters must be preserved
This is code-golf so naturally, fewest bytes wins!

The Testcases
Test cases are separated with <===========>, with a blank line between input and expected output
Hello, World!

Keppo, Jotpp!
<===========>
Yes

Yey
<===========>
TestcaSe

PeypyaYe
<===========>
Programming Puzzles and Code Golf

Ktojtassitj Kubbpey atp Yope Jopt
<===========>
This text has a
newline in it

Pkiy pekp kay a
tejpite it ip
<===========>
Corpus language best language

Yotkuy patjuaje teyp patjuaje
<===========>
Strip the flesh! Salt the wounds!

Yptik pke tpeyk! Yapp pke joutpy!
<===========>
"Install Warframe" they said, "It'll be fun" they said

"Itypapp Jatttase" pkey yaip, "Ip'pp te tut" pkey yaip
<===========>
What the **** did you just ****ing say about me, you little *****?
I'll have you know I graduated top of my class in the Navy Seals,
and I've been involved in numerous secret raids on Al-Quaeda,
and I have over 300 confirmed kills.

Jkap pke **** pip you tuyp ****itj yay atoup se, you pipppe *****?
I'pp kate you ktoj I jtapuapep pok ot sy ypayy it pke Taty Yeapy,
atp I'te teet ittoptep it tusetouy yeytep taipy ot Ap-Ruaepa,
atp I kate otet 300 yottitsep kippy.

The Bonus
If you also include an audio recording (or video with audio) of you pronouncing each of the testcase's Corpus translations, you may multiply your bytecount by 1 as a reward.

@Shaggy Sure, shoulda thought of that

@RobertS. it also provides no bonus to your score (x * 1 == x). It's a tongue-in-cheek poke at the unpronouncability of many of the translations

Can output be an array of characters?

@Skidsdev The "translations" are pronounceable; either as a recorded human voice or programmatically using machine learning and/or text-to-speech technologies.

@ngn oops my bad, copied from reddit :P

I showed this to a friend and now he's translating his texts. It's really hard to translate back because it's lossy (not one-to-one).
Help.

And so was born the Finnish language!

Bash + coreutils, 46
tr b-zB-Z ${a=typetjkitkpstokrtyputjkyb}${a^^}

Try it online!

T-SQL, 107 Bytes
SELECT TRANSLATE(v,'bcdfghjlmnpqrstvwxzBCDFGHJLMNPQRSTVWXZ',
                   'typtjktpstkrtyptjkbTYPTJKTPSTKRTYPTJKB')FROM t

Line break and indent are for readability only.
Uses the SQL 2017 function TRANSLATE for character replacement.
Unfortunately had to put (almost) the entire alphabet in there twice to maintain casing. There are probably better ways, maybe something that handles the t's as a group, but this worked for me.
Input is via a pre-existing table t with varchar column v, per our IO rules.
In this case the table must be created using a case-sensitive collation, either by running on a case-sensitive server, or by using the COLLATE keyword (not counted toward character total):
CREATE TABLE t(v varchar(999) COLLATE Latin1_General_CS_AS)

05AB1E, 24 bytes
ži.•ÜÁ©;«ìñä°ÔG·ÖYΘ•Du«‡

Explanation:
                       ‡   transliterate
ži                         a-zA-Z with
  .•ÜÁ©;«ìñä°ÔG·ÖYΘ•       atypetjkitkpstokrtyputjkyb
                    Du«    concatenated with itself, uppercased

Try it online!

Minor 24 bytes alternative by transliterating just the consonants.

Charcoal, 42 bytes
≔”$⌊∧Ｔ-¶I^F⁷ü@n⁹γ›7η”η⭆θ⎇№β↧ι§⎇№βι↧ηη⌕β↧ιι

Try it online! Link is to verbose version of code. Explanation:
≔                       Assign
 ”$⌊∧Ｔ-¶I^F⁷ü@n⁹γ›7η”  Compressed string `ATYPETJKITKPSTOKRTYPUTJKYB`
                     η  To variable

 θ                      Input string
⭆                       Map over characters and join
  ⎇                     If
    β                   Lowercase alphabet
   №                    Contains
      ι                 Current character
     ↧                  Lowercased
                        (Then)
        ⎇               If
          β             Lowercase alphabet
         №              Contains
           ι            Current character
                        (Then)
             η          Variable
            ↧           Lowercased
                        (Else)
              η         Variable
       §                Indexed by
               ⌕        Index of
                  ι     Current character
                 ↧      Lowercased
                β       In lowercase alphabet
                        (Else)
                   ι    Current character
                        Implicitly print

JavaScript (Node.js), 100 bytes
s=>s.replace(/[a-z]/gi,c=>(C='_atypetjkitkpstokrtyputjkyb'[Buffer(c)[0]&31],c>{}?C:C.toUpperCase()))

Try it online!
s =>                                  // s = input string
  s.replace(                          // replace in s ...
    /[a-z]/gi,                        // ... all letters, no matter the case
    c => (                            // for each letter c:
      C =                             //   pick a replacement letter C
        '_atypetjkitkpstokrtyputjkyb' //   from a 1-indexed lookup string
        [Buffer(c)[0] & 31],          //   using the 5 lowest bits of the ASCII code of c
      c > {} ?                        //   if the original letter was in lower case:
        C                             //     use the replacement letter as-is
      :                               //   else:
        C.toUpperCase()               //     convert it to upper case
    )                                 //
  )                                   // end of replace()

R, 79 bytes
function(x)chartr("b-zB-Z",paste0(y<-"typetjkitkpstokrtyputjkyb",toupper(y)),x)

Try it online!
Only 3 bytes shorter than literally typing both lower- and uppercase replacement strings together...
R, 79 bytes
function(x,`[`=chartr)"B-Z"[toupper(y),"b-z"[y<-"typetjkitkpstokrtyputjkyb",x]]

Try it online!
Fancier code, same byte count.

Perl 5 -p, 54 bytes
eval"y/B-Zb-z/".uc($a=typetjkitkpstokrtyputjkyb)."$a/"

Try it online!

C# (Visual C# Interactive Compiler), 151 83 bytes
n=>n.Select(a=>char.IsLetter(a)?(char)(" ATYPETJKITKPSTOKRTYPUTJKYB"[a%32]|a&32):a)

Try it online!
Saved a whopping 68 bytes thanks to Pietu1998!

You can get this down to 83 bytes by removing the range, combining the Selects and using bitwise operators for casing.

Thanks! Nice trick with the bit wise operators!

Ruby, 55 bytes
$_.tr!"b-zB-Z",(x="typetjkitkpstokrtyputjkyb")+x.upcase

Try it online!

J, 78 61 bytes
-17 bytes thanks to FrownyFrog!
rplc(u:,98 66+/i.25);"0(,toupper)@'typetjkitkpstokrtyputjkyb'

Try it online!

61

@FrownyFrog Thanks! In my J804 I can't use constants at the right-hand side. I need to upgrade :)
My solution is ugly nevertheless....

K (ngn/k), 87 63 61 60 59 bytes
f:{`c$x-a-@[!128;98+!25;:;0+"typetjkitkpstokrtyputjkyb"]a:_x}

Try it online!
{ } function with argument x
_ to lowercase
a: assign to a
!128 the list 0 1 2 ... 127
@[!128; indices ;:; values ] amend it by replacing the elements at indices with the given values
98+!25 the list 98 99 100 ... 122 which are the ascii codes for "bcd...z"
juxtaposition is indexing, so we use a (implicitly converted to integers) as indices in the amended list
x-a- subtract from a, then subtract form x; this restores upper/lowercase as in the original string
`c$ convert to characters

Retina 0.8.2, 59 bytes
[a-z]
 $&
T`l`L
T`L`ATYP\ETJKITKPST\OKRTYPUTJKYB
T`L `l_` .

Try it online! Link includes test cases. Explanation:
[a-z]
 $&

Prefix a non-ASCII character to each letter. (I tried to use a non-breaking space but Firefox might have changed it back to a regular space after copying and pasting.)
T`l`L

Uppercase all of the letters.
T`L`ATYP\ETJKITKPST\OKRTYPUTJKYB

Convert the letters to Corpus.
T`L `l_` .

Lowercase the letters after the non-ASCII character and delete the non-ASCII character.
Naïve solution is 63 bytes:
T`lL`aty\petjkitk\pst\okrty\putjkybATYP\ETJKITKPST\OKRTYPUTJKYB

Try it online! Link includes test cases.

Python 2, 115 bytes
lambda i:i.translate(''.join(a))
a=map(chr,range(256))
b='typetjkitkpstokrtyputjkyb'
a[66:91]=b.upper();a[98:123]=b

Try it online!
Adapted from @ElPedro's answer. This generates the replacement for every ascii character in the array a and then uses str.translate.
Can be brought to 113 bytes if returning an array of chars is acceptable:
lambda i:[a[ord(x)]for x in i]
a=map(chr,range(256))
b='typetjkitkpstokrtyputjkyb'
a[66:91]=b.upper();a[98:123]=b

Try it online!

Took some thinking at this time of night to work out how it was doing it. It's been a long day! Nice solution.

Jelly, 29 bytes
“qḍỊYl4ḃ©ƭṄxḳ½Dy’ṃⱮØA,Øa¤⁺żẎy

Try it online!

Only need to map consonants, so “ØṖ¦Mṗẏʋ@¥;}/’ṃⱮØCŒH¤⁺żẎy saves 4. (Can't get rid of 'y' with ØY since it's on the right of the map)

Japt, 37 bytes
;®i`…ypetjkŠkp¡okrtyp©jkyb`pu)gCpu bZ

Try it online!
The above link includes most test cases, but I haven't been able to input the second to last one as a string because it contains both single and double quotes. Here is that test case, input as an array of characters instead.
Explanation:
;                                        #Set C to the lowercase alphabet
 ®                                       #For each character Z of the input:
                                   bZ    # Find its index in:
                               C         #  The lowercase alphabet
                                pu       #  Plus the uppercase alphabet
                                         #  (-1 if it is not a letter)
                              g          # And get the character at that index in:
   `…ypetjkŠkp¡okrtyp©jkyb`              #  Get the lowercase Corpus alphabet
                           pu)           #  Plus the uppercase Corpus alphabet
  i                                      #  Add Z at the end (index -1)

Pyth, 34 chars
J."byàHuH¯¹e?rJyfh"XXztGJrtG1rJ1
Try it online!
The string is a compressed version of "typetjkitkpstokrtyputjkyb". For some reason I cannot get Pyth to compress "atypetjkitkpstokrtyputjkyb" with the function
+++\.N++hSzeSzCi-RChSzCMz-hCeSzChSzN
even though it would probably save a byte or two as the two t's could be eliminated.

Tcl, 114 bytes
proc C s {string map [split {btcydpftgjhkjtlpmsntpkqrrtsytpvtwjxkzbBTCYDPFTGJHKJTLPMSNTPKQRRTSYTPVTWJXKZB} ""] $s}

Try it online!

Python 2, 138 132 bytes
def f(i,a='bcdfghjlmnpqrstvwxz',b='typtjktpstkrtyptjkb'):a+=a.upper();b+=b.upper();return''.join((x,b[a.find(x)])[x in a]for x in i)

Try it online!
Actually shorter with a named function than with a lambda!
lambda version, 138 bytes
lambda i,a='bcdfghjlmnpqrstvwxzBCDFGHJLMNPQRSTVWXZ',b='typtjktpstkrtyptjkbTYPTJKTPSTKRTYPTJKB':''.join((x,b[a.find(x)])[x in a]for x in i)

Try it online!
131 125 if we are allowed to return an array of characters and have the join outside the function.
def f(i,a='bcdfghjlmnpqrstvwxz',b='typtjktpstkrtyptjkb'):a+=a.upper();b+=b.upper();return[(x,b[a.find(x)])[x in a]for x in i]

Try it online!

You can still use a lambda if you make a and b global variables

@BlackOwlKai - Thanks. Have been playing with that idea but it's getting late. Maybe tomorrow :)

Got it down to 120/113 characters (TIO without the testcases, else the link would be too long for a comment)

Beats mine and different enough for you to post as your own answer. I'll upvote :)

Feel free to steal my test cases as well. They took longer to type in than the code...

PHP, 100 bytes
Code

<?=strtr($argv[0],($u=bcdfghjklmnpqrstvwxyz).($f=strtoupper)($u),($v=typtjktkpstkrtyptjkyb).$f($v));

Try it online!
Explanation

# using the provided character mapping given on the question, as an array
strtr($argv[0],                                  # strtr for the string replacement
($u=bcdfghjklmnpqrstvwxyz).($f=strtoupper)($u),  # Taking onthe the keys for the first string
($v=typtjktkpstkrtyptjkyb).$f($v));              # Using the values for second string
 # In both Strings appending the upper version of each string.

Pyth, 35 Bytes
XQ.*m+r1dd[tG."byàHuH¯¹e?rJyfh

Try it!
Could probably use Left map or something to get rid of the two d's, but I couldn't figure out how.

Java, 211 Bytes
a->{String b="BCDFGHJLMNPQRSTVWXZbcdfghjlmnpqrstvwxz",c="TYPTJKTPSTKRTYPTJKBtyptjktpstkrtyptjkb";a.chars().map(x->{int z;return(z=b.indexOf(x))>-1?c.toCharArray()[z]:x;}).forEach(x->System.out.print((char)x));};

Try it online!

191 bytes

Lua, 153 bytes
t="atypetjkitkpstokrtyputjkyb"for c in(...):gmatch"."do i=c:lower():byte()-96n=t:sub(i,i)io.write((i<0or i>26)and c or(c==c:upper()and n:upper()or n))end

Try it online!


text:
Does this Set Represent a Natural Number?
In set theory, the natural numbers \$\mathbb{N} = \{0, 1, 2, 3, ...\}\$ are usually encoded as pure sets, that is sets which only contain the empty set or other sets that are pure. However, not all pure sets represent natural numbers. This challenge is about deciding whether a given pure set represents an encoding of natural number or not.
The encoding of natural numbers works in the following way1:

Zero is the empty set: \$ \text{Set}(0) = \{\} \$
For a number \$n > 0\$: \$ \text{Set}(n) = \text{Set}(n-1) \cup \{\text{Set}(n-1)\}\$

Thus, the encodings of the first few natural numbers are

\$ 0 \leadsto \{\}\$
\$ 1 \leadsto \{0\} \leadsto \{\{\}\}\$
\$ 2 \leadsto \{0,1\} \leadsto \{\{\},\{\{\}\}\}\$
\$ 3 \leadsto \{0,1,2\} \leadsto \{\{\},\{\{\}\},\{\{\},\{\{\}\}\}\}\$
\$ 4 \leadsto \{0,1,2,3\} \leadsto \{\{\},\{\{\}\},\{\{\},\{\{\}\}\},\{\{\},\{\{\}\},\{\{\},\{\{\}\}\}\}\}\$

The Task

Given a string representing a pure set, determine whether this set encodes a natural number according to the above construction.
Note, however, that the elements of a set are not ordered, so \$\{\{\},\{\{\}\},\{\{\},\{\{\}\}\}\}\$ is not the only valid representation of \$3\$ as e.g. \$\{\{\{\}\},\{\},\{\{\{\}\},\{\}\}\}\$ represents the same set.
You may use [], () or <> instead of {}.
You may assume the sets are given without the , as separator.
You can assume there won't be any duplicate elements in the input, e.g. {{},{}} is not a valid input, and that the input is well-formed, e.g. no {{},, {,{}} or similar.

Test Cases
True:
{}
{{}}
{{},{{}}}
{{{}},{}}
{{},{{}},{{},{{}}}}
{{{},{{}}},{},{{}}}
{{{{}},{}},{{}},{}}
{{},{{}},{{},{{}}},{{},{{}},{{},{{}}}}}
{{{{{}},{}},{{}},{}},{{}},{},{{},{{}}}}
{{},{{}},{{},{{}},{{},{{}}},{{},{{}},{{},{{}}}}},{{{}},{}},{{},{{}},{{},{{}}}}}
{{{{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}}

False:
{{{}}}
{{{{}}}}
{{{{}},{}}}
{{},{{}},{{{}}}}
{{{},{{}}},{{}}}
{{{{{}}},{}},{{}},{}}
{{},{{}},{{},{{}}},{{},{{}},{{{}}}}}
{{{{{}},{}},{{{}}},{}},{{}},{},{{},{{}}}}
{{{{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}},{{{{{}},{}},{{}},{}},{{{}},{}},{{}}},{{{{}},{}},{{}},{}},{{{}},{}},{{}},{}}

Related: Natural Construction (Output the set encoding of a given natural number.)
1 See https://en.wikipedia.org/wiki/Set-theoretic_definition_of_natural_numbers

The test cases look like a program in a (yet) unimplemented esolang :)

can the input be a data structure (nested lists) instead of a string?

I thought it was Brain-flak for a moment.

Just for clarification, it seems to be implied, but the input will always be well formed?  e.g. no {{}, {,{}} or {}{{},{{}}}

@ngn No, input needs to be a string.

@Laikoni, if only string input is acceptable, I think this basically invalidates current top answers.

This reminds me of surreal numbers, I should make a challenge about them!

@PostLeftGarfHunter Yes, the input will always be well-formed.

@KirillL. Technically these answers weren't valid to begin with as the challenge always stated "Given a string representing a pure set", though I do see the point that allowing nested data structures allows for interesting golfing opportunities. However, I find it hard to decide where to draw the line on what is an allowed data structure and what isn't to avoid abuse of a too lenient input format, so I decided to restrict inputs to strings for the sake of simplicity and unambiguousness.

@GalenIvanov If you go about inventing a language inspired by this question you could make it so that the task in this question could be implemented using the empty string. (That of course doesn't qualify as an answer as I believe answers have to be written in a language which existed before the question was asked.)

@kasperd Languages created after a challenge was made are now competing.

@GalenIvanov Reminds me of ObCode ... Guess I have to try solving the challenge in it!

@JonathanAllan Thanks for letting me know. I reformulated the first sentence, though I'm not sure whether it is much clearer now as for me it was clear from the beginning that all numbers are encoded as pure sets, but not all pure sets represent such an encoding. If you have a suggestion on how to aid understanding, please let me know.

JavaScript (Node.js), 53 48 44 bytes
f=a=>(a=eval(a)).every(e=>a[e.length]&&f(e))

Try it online! Test cases mostly shamelessly stolen from @Arnauld's answer. Explanation: If a set represents a natural number, then the natural number it represents must be equal to the size of the set, and (given that the elements are guaranteed distinct) the elements must be the representations of the natural numbers less than it, and these must therefore have shorter lengths. This is trivially true of the empty set of course. Edit: Saved 5 bytes thanks to @Arnauld. Saved 4 bytes thanks to @Cowsquack.

!e[a.length-1] should save 3 bytes

@Arnauld Or better still, a[e.length]&& for 5 bytes!

@JoKing Ugh, I just copied Arnauld... string input costs 14 bytes :-(

Surely, g=(A,a=eval(A))=>a.every(e=>a[e.length]&&g(e)) would work?

@Cowsquack Ah, nice, that actually saves 4 bytes, thanks!

Python 3, 69 58 44 bytes
11 bytes thanks to Erik the Outgolfer.
14 bytes thanks to Mr. Xcoder.
f=lambda s:all(len(e)<len(s)*f(e)for e in s)

Try it online!

@Mr.Xcoder done

Oh wow, nice improvement!

@Mr.Xcoder and then now I realize that it's the same as Neil's answer... so techincally Neil defeated me

Wolfram Language (Mathematica), 60 59 bytes
E!=(If[Sort@#===Range[t=Tr[1^#]]-1,t,E]&//@ToExpression@#)&

Try it online!
The core of this solution is the function
If[Sort@#===Range[t=Tr[1^#]]-1,t,E]&

which converts a list of the form {0,1,2,...,n-1} in any order into the output n (in particular, it converts {} to 0), and converts anything else into the real number E.
Call this function f. Given an input such as "{{{}},{}}", we do the following:

Convert the string into a Mathematica expression.
Apply f at every level, getting f[{f[{f[{}]}], f[{}]}].
Evaluating all the f's will produce a natural number for an input representing it. For example, f[{f[{f[{}]}], f[{}]}] = f[{f[{0}], 0}] = f[{1, 0}] = 2. Anything else will produce E.
We test if the result is a natural number by checking if it's not E.

Brachylog (v2), 9 bytes
↰ᵐo.t~k|Ė

Try it online!
As usual for a decision-problem, this is a full program. Input from standard input, using square brackets. Output to standard output as true. versus false..
Explanation
Although I said above that this is a full program, it's actually more interesting than that; it's both a full program and a function. When used as a full program, it prints true. if the set is a natural number, or false. if it isn't. When used as a function, it "normalizes" a natural number (i.e. normalizes all its elements and sorts them into order by value; this program uses lists internally, not sets), or "throws an exception" (actually a failure, as this is Prolog) if the input isn't a natural number.
The full program behaviour is easy enough to explain: it's purely implicit in Brachylog's treatment of full programs that don't contain I/O instructions. The behaviour in question is "run the function, taking its input from standard input, and asserting that its output matches the description given by the first command-line argument; if the assertion fails or the program throws an exception, print false., otherwise print true.". In this case, the command-line argument is missing (i.e. "anything goes"), so the exception/no-exception behaviour of the function gives the output.
As for the function behaviour:
↰ᵐo.t~k|Ė
↰ᵐ          Map a recursive call to this function over the list
  o         Sort the list
   .   |    Assert that the following operation need not change the list:
    t         Take the last (i.e. greatest) element of the list
     ~k       Append an arbitrary element to the resulting list
   .   |    Output the unchanged list
       |    Exception handler: if the above threw an exception,
        Ė     Assert that the input is empty, and output an empty list

A natural number is defined as containing two parts: the elements of the natural number below, unioned with the number itself. Thus, all its elements are also natural numbers. We can recognise a natural number by a) verifying that all its elements are natural numbers, b) verifying that the set's largest element is identical to the set without its largest element.
When we're using lists rather than sets (thus the square brackets), we need to put them into a consistent order for equality comparisons to work (in this case, sorted by "value"). Brachylog's default sort order will sort a prefix of a list before the list itself, which conveniently means that it'll sort natural numbers by numerical value. So we can just recursively sort all our numbers to get them into a consistent order. In fact, via the function we're defining recursively, we can achieve both results at the same time: recursively sorting the number's elements, and verifying that it's a natural number.
The function thus has four main parts. ↰ᵐ is the recursive call, ensuring that each element is a natural number, and converting it each element to a normalized form. o the normalizes the number itself (its elements are already normalized, so all we have to do is to sort it). Then .t~k| ensures we have the structure we want by checking that the largest element and other elements are the same. An empty list (i.e. 0) doesn't have a last element, so will get an assertion failure with t; the |Ė handles this case, via giving an explicit fallback in the case where the input list is empty.

Jelly, 8 bytes
ß€Ṣ
ÇṖƤƑ

Since the input has to be a string, this submission is only valid as a full program.
Try it online! or verify all test cases
How it works
ß€Ṣ   Helper link. Argument: A (array)

ß€    Recursively map the helper link over A.
  Ṣ   Sort the result.

This yields a canonical representation of the input, consisting solely of sorted arrays.
ÇṖƤƑ  Main link. Argument: A (array)

Ç     Call the helper link to canonicalize the array.
   Ƒ  Fixed; call the link to the left and test if it returns its argument unchanged.
 ṖƤ       Pop prefix; for each non-empty prefix of the result, remove its last element.

K (ngn/k), 26 24 27 bytes
{{o'!x}[#x]~{x@<x:o'x}x}`j?

Try it online!

Jelly, 7 bytes
Ẉ<Laß€Ạ

This is a port of Leaky Nun's Python answer.
Since the input has to be a string, this submission is only valid as a full program.
Try it online! or verify all test cases
How it works
Ẉ<Laß€Ạ  Main link. Argument: A (array)

Ẉ        Width; compute the length of A's elements.
  L      Yield the length of A.
 <       Compare them, yielding an array of Booleans.
    ß€   Recursively map the main link over A.
   a     Take the logical AND of the Booleans and the results of the map.
      Ạ  All; yield 1 if and only if all ANDs yielded 1.

Wolfram Language (Mathematica), 36 40 bytes
(Sort//@#//.{a__,{a__}}:>{a})!={}!={{}}&

Try it online!

Jelly, 10 bytes
ẈṢ‘⁼Jȧ@ßƇƑ

Try it online!

Japt, 9 bytes
Port of Neil's JS solution. Please upvote that if you're upvoting this.
e@Ê>XÊ©ßX

Try it or run all test cases
              :Implicit input of array U
e             :Does every element in U return true
 @            :When passed through the following function as X
  Ê           :Length of U
   >          :Greater than
    XÊ        :Length of X
      ©       :Logical AND with
       ßX     :A recursive call of the programme with X passed as the new value of U

Red, 81 bytes
func[a][p: 1 foreach e to-block a[p: p *(pick[1 0](length? e)< length? a)* f e]p]

Try it online!
Similar to Leaky Nun's Python 3 answer

Wolfram Language (Mathematica), 52 bytes
If[Max[#0/@#]<(l=2Length@#),l]&@*ToExpression/*EvenQ

Try it online!

source: Stack Exchange

license: Creative Commons - Attribution Share-Alike - https://creativecommons.org/licenses/by-sa/4.0/

sort: votes


text:
Just Twice Is Nice
Let's have string multiplication to take a string and a positive number and concatenate that string that many times.  In Haskell:
0 * s = ""
n * s = s ++ ((n - 1) * s)

Your task is to write a program that outputs a number \$n > 0\$.  When you double your program (with the described procedure) the new program should output \$2n\$.  When your program is tripled or quadrupled it should once again output the original \$n\$.
This is code-golf so your answers will be scored in bytes with fewer bytes being better.

Related, Related.

All the solutions so far are trivial modifications of solutions to existing challenges so I wouldn't be surprised to see this dupe-hammered by someone.

@Shaggy I disagree.  For some the best approach is from the 3rd times the charm from some its the doubling, for some still (klein, lost etc.) a port wouldn't even work.

Python 2, 9 bytes
Outputs via exit code. Full credit goes to Anders Kaseorg for this answer to I double the source, you double the output!.
';exit(2)

Try it online! | Doubled | Tripled | Quadrupled
Regularly, this raises a Syntax Error because the string is not closed properly and exists with exit code 1, then, when doubled, it becomes ';exit(2)';exit(2) which simply exits with code 2 because the string literal is now quoted properly and has no effect at all, and when repeated any other arbitrary number of times, it raises Syntax Errors.

R, 27 25 bytes
1+!1-length(readLines())

Try it online!
Inspired by rturnbull's answer to Third time the charm, but the simplicity of the check makes it shorter.
Doubled | Tripled | Quadrupled
Why this works:
readLines() will actually reads the source file itself rather than stdin. Hence, adding lines just increments the length() of the vector returned by readLines(). Therefore, we compute !(1-length()) to obtain 1 whenever length()==1 and 0 when length()!=1, adding one to have the desired effect.

05AB1E, 7 6 5 bytes, n=3
₆¾Θè¼

-1 byte thanks to @CommandMaster.
Try it online; try it doubled; try it tripled; try it quadrupled.
Explanation:
₆      # Push 36
 ¾     # Push the counter_variable (0 by default)
  Θ    # 05AB1E-style truthify: ==1 (results in 1 if 1; 0 for everything else)
   è   # Use that to index into the 36
    ¼  # Increase the counter_variable by 1
       # (and output the indexed digit implicitly as result)

5 bytes: ₆¾¼Θè (uses the counter variable and thruthifies, so it doesn't have to compare to two) Try it online; Doubled; Tripled; quadrupled

@CommandMaster Smart! Thanks for the -1. :)

Klein 000, 6 bytes
\1+@
\

Try it online!
Explanation
The idea here is pretty simple, the first 2 copies require the ip to move off the edge of the program but the third does not, that way adding anything extra after the third doesn't change anything.
The first program reflects off of both edges the
\
\

At the begining is just a fancy noop. So we just run 1+@.
For the two copies this gets interrupted
\1+@
\\1+@
\

We hit both 1s and a + to get 2.
On the three we interrupt the path even further
\1+@
\\1+@
\\1+@
\

At this point it might not be the clearest, but the ip bounces around in the mirrors a bit before it hits runs the 3rd line.
This is what it runs
\
\\
 \1+@

Since this doesn't go off an edge further additions don't make any change to the program.

Runic Enchantments, 29 bytes
^w '\
/1@ 3
/ ~!4
2'51\w
/yyy

Try it online!
Slight alteration from the Third Times A Charm entry, using Jo King's compressed version and swapping the two reflection locations.
Twice
Thrice
And frice for good measure.

What did you name the language after? God of War?

@Riker I named it because I want to (eventually) have a RPG-esque game where enchantments on gear can be created by players using a runic script (that, to the best of my ability) mapped to the 95 keyboard printable characters. I've added some extension symbols utilizing unicode combining characters (as well as a handful of other unicode characters that more readily represent the given action, such as π and several arrows). Once I have that game I'll end up making "runic 2.0" that has those game-specific commands (and probably remaps a lot of mathy stuff).

Draco: huh, interesting, I'd love to see the final result.

Charcoal, 10 bytes
ＰＩ⊕⁼²Ｌ⊞Ｏυω

Try it online! Based on my answer to I double the source, you double the output! but compares the length to 2. Try it doubled. Try it tripled. Try it quadrupled. In verbose syntax this is Multiprint(Cast(Incremented(Equals(2, Length(PushOperator(u, w)))))));.

MATL, 9 bytes
vxHQXH4=Q

Uses n = 1.
Try it online! Original, doubled, tripled, quadrupled.
Explanation
This uses clipboard H to store state information. Function H pastes the clipboard contents onto the stack. Function XH copies the top of the stack into the clipboard. The clipboard initially contains the number 2.
Each time the snippet vxHQXH4=Q is run it does the following.
The stack contents, if any, are deleted (vx). The clipboard contents are pushed (H) and incremented (Q), and the result is copied back into the clipboard (XH). This gives 4 the second time, and only that time.
The number in the stack is tested for equality with 4 (4=) and incremented (Q). This gives 2 for 4 (second time), and 1 otherwise (any other time).

Jelly, 6 4 bytes (n=1)
»ɼḂ‘

Try it online!
Explanation
Initial value = 0
Initial register value = 0
 ɼ     Set the register and current value to
»        maximum of the register and current value
  Ḃ    Parity
   ‘   Increment

Brain-Flak, 36 bytes
(({}<>{})[()]<>)(()(){[()](<{}>)}{})

Try it online!
Uses a counter off stack.  Each time we run the counter is incremented by the result of the last run.  If the counter is equal to exactly 1 (which it is when the program is doubled) then we push 2, otherwise 1 is pushed.
The selection is done using a bit of a weird if statement.  It is 2 normally but if the if is entered (which it is when the count is not 1) we subtract 1 from it.

Bash, 8 bytes
This is a rip-off of the Python 2 answer. Output is via exit code.
Unlike the Python 2 answer, this uses \$n=2\$.
";exit 4

Try it online!
Try it online (doubled)!
Try it online (tripled)!
Try it online (quadrupled)!

PowerShell, 24 bytes
';$host.setshouldexit(2)

Try it online! | Doubled | Tripled | Quadrapled
Outputs through exit code, Shameless port of python answer ;-)

Vyxal, 4 bytes (n=1)
∴:∷›

Try it Online!
Same approach as my Jelly answer, but much more aesthetic.
∴:∷›
∴    Pop a, b; push max(a,b)
 :   Duplicate
  ∷  Parity
   › Increment

05AB1E, 4 bytes (n=1)
.gΘ>

Try it online!
Explanation
.gΘ>
.g   Push length of stack
  Θ  Equals 1?
   > Increment

all_licenses: Creative Commons - Attribution Share-Alike - https://creativecommons.org/licenses/by-sa/4.0/


text:
Dungeon Crawler
Input

A binary matrix \$M\$ representing the walls of a dungeon.
The position \$(x,y)\$ of the player within the dungeon.
The direction \$d\$ that the player is currently facing (0 = North, 1 = East, 2 = South, 3 = West)

Output
A pseudo-3D representation of the walls that are in the field of view of the player, as an ASCII art of \$30\times 10\$ characters.
Below are several possible output frames, along with the corresponding map and compass to help getting the hang of it (but drawing the map and the compass is not part of the challenge).

Specification
Field of view
The player has \$13\$ walls in his field of view, labeled from \$A\$ to \$M\$. Below are the positions of the walls relative to the player (in yellow), in all possible directions.

Drawing the walls
The walls are supposed to be drawn from \$A\$ to \$M\$ in this exact order, given that any part drawn previously may be overwritten by closer walls. You may of course implement it differently as long as the final result is the same.
The entire output is drawn with 7 distinct characters: " ", "'", ".", "|", "-", "_" and ":".
Because detailing the shapes of the walls in the body of this challenge would make it too lengthy, they are instead provided in the following TIO link:
Try it online!
The characters that are not part of a given wall are marked with a "?" in these diagrams. They must be treated as 'transparent' characters that are not drawn at all. On the other hand, all spaces within a wall are 'solid' and must overwrite any other characters that may have been previously drawn there.
Rules
About the input

You may take \$M\$, \$x\$, \$y\$ and \$d\$ in any reasonable format.
You may use either 0-indexed or 1-indexed coordinates.
You may use 4 distinct values of your choice for the directions.
The matrix is guaranteed to be at least \$3\times 3\$.
You may assume that there will always be surrounding walls on the edges.
The player is guaranteed to be located on an empty square.
The input is guaranteed to be valid.

About the output

The walls must be drawn exactly as described.
However, the output format is also flexible: single string, array of strings, matrix of characters, etc.
Leading and trailing whitespace is acceptable as long as it's consistent.

This is code-golf.
Test cases
All test cases are using the following matrix:
[ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
  [ 1, 0, 1, 1, 1, 0, 0, 0, 0, 1 ],
  [ 1, 0, 1, 0, 1, 0, 0, 1, 0, 1 ],
  [ 1, 0, 0, 0, 0, 0, 1, 1, 0, 1 ],
  [ 1, 0, 0, 1, 0, 0, 0, 1, 0, 1 ],
  [ 1, 0, 0, 1, 1, 0, 1, 1, 0, 1 ],
  [ 1, 1, 1, 1, 0, 0, 0, 0, 0, 1 ],
  [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] ]

The following inputs are using 0-indexed coordinates, with \$(0,0)\$ pointing to the top-left corner.
x=3, y=3, d=0
x=6, y=4, d=3
x=4, y=4, d=1
x=1, y=5, d=2
x=7, y=7, d=3
x=6, y=6, d=1
x=8, y=1, d=2
x=7, y=6, d=1

Expected outputs:
------------------------------    ------------------------------
 x=3, y=3, d=0:                    x=6, y=4, d=3:
------------------------------    ------------------------------
__                          __    '.                          .'
  |'.                    .'|        |                        |
  |   '.--------------.'   |        |----.                   |
  |    |              |    |        |    | '.--------.       |
  |    |              |    |        |    |  |        |       |
  |    |              |    |        |    |  |        |       |
  |    |              |    |        |    | .'--------'       |
  |   .'--------------'.   |        |----'                   |
__|.'                    '.|__      |                        |
                                  .'                          '.
------------------------------    ------------------------------
 x=4, y=4, d=1:                    x=1, y=5, d=2:
------------------------------    ------------------------------
                            .'    __ ________________________ .'
                           |        |                        |
-------.              .----|        |                        |
       | '.--------.' |    |        |                        |
       |  |        |  |    |        |                        |
       |  |        |  |    |        |                        |
       | .'--------'. |    |        |                        |
-------'              '----|        |                        |
                           |      __|________________________|
                            '.                                '.
------------------------------    ------------------------------
 x=7, y=7, d=3:                    x=6, y=6, d=1:
------------------------------    ------------------------------
'.                                '.
  |'.                               |'.
  |   '.                            |   '.
  |    | '.                 .-      |    |--.--------.--------.-
  |    |  |:               :|       |    |  |        |        |
  |    |  |:               :|       |    |  |        |        |
  |    | .'                 '-      |    |--'--------'--------'-
  |   .'                            |   .'
  |.'                               |.'
.'                                .'
------------------------------    ------------------------------
 x=8, y=1, d=2:                    x=7, y=6, d=1:
------------------------------    ------------------------------
'.                          __    '.
  |'.                    .'|        |
  |   '.              .'   |        |----.--------------.-------
  |    | '.        .' |    |        |    |              |
  |    |  |:      :|  |    |        |    |              |
  |    |  |:      :|  |    |        |    |              |
  |    | .'        '. |    |        |    |              |
  |   .'              '.   |        |----'--------------'-------
  |.'                    '.|__      |
.'                                .'

Related challenge:
This challenge from 2013 is closely related. But it has a different winning criterion (code-challenge), a much looser specification of the output, and requires interactive I/O.

This instantly reminded me of 3D Monster Maze, although that uses block graphics of course.

Your challenges are so fun and well-written!

Waiting for a solution in Minecraft...

Does anyone else remember the windows screen saver? Was such a fun "game" when I was 5 or 6...

Clean (with Snappy), 800 785 670 644 bytes
460 402 bytes of code + 360 242-byte string literal
(escaped here and on TIO because it isn't valid UTF-8)
You can verify the length of the literal here.
import StdEnv,Data.List,Data.Maybe,Codec.Compression.Snappy,Text
@a b|b<'~'=b=a
$m x y d=map(@' ')(foldl(\a b=[@u v\\u<-a&v<-b])['~~'..][join['
']k\\Just(Just 1)<-[mapMaybe(\e=e!?(x+[u,v,~u,~v]!!d))(m!?(y+[~v,u,v,~u]!!d))\\u<-[-2,2,-1,1,0,-1,1,0,-1,1,0,-1,1]&v<-[3,3,3,3,3,2,2,2,1,1,1,0,0]]&k<-nub[q\\w<-split"#"(snappy_uncompress"\211\6\44\41\41\41\55\56\40\41\40\174\72\5\4\60\55\47\40\41\41\41\43\41\41\41\176\56\55\r\1\24\56\40\41\176\174\40\r\1\4\174\72\72\r\0\0\47\r\46\35\72\25\1\31\103\0\41\25\24\35\113\176\25\0\31\133\11\224\r\152\20\56\40\40\40\41\21\217\10\40\47\56\31\14\4\40\174\126\14\0\4\56\47\21\74\0\47\1\74\1\340\r\220\25\242\11\1\25\250\25\360\11\1\25\253\376\30\0\21\30\25\333\11\1\24\47\41\41\43\137\137\11\154\20\41\40\40\174\47\r\344\1\157\5\341\1\11\5\336\172\11\0\34\56\47\41\137\137\174\56\47\1\347\20\43\176\176\40\137\132\1\0\4\40\41\75\211\76\1\0\1\356\5\150\116\1\0\376\35\0\376\35\0\126\35\0\132\347\0\20\137\174\41\43\47\101\337\51\74\41\133\122\4\0\10\56\47\40"),q<-let l=[[c\\c<-:rpad s 30'~']\\s<-split"!"w]in[l,map reverse l]]])

Try it online!
Snappy compression actually does pretty well in this case, despite being speed-focused, because there are so many single-character runs in the string being compressed.
The uncompressed string (with # replaced with \n for clarity) is:
!!!-. ! |:! |:!-' !!!
!!!~.--------. !~|        |:!~|        |:!~'--------' !!!
!!!~~~~~~~~~~.--------.!~~~~~~~~~~|        |!~~~~~~~~~~|        |!~~~~~~~~~~'--------'!!!
!!-------.   !       | '.!       |  |!       |  |!       | .'!-------'   !!
!!~~~~~~~.--------------.!~~~~~~~|              |!~~~~~~~|              |!~~~~~~~|              |!~~~~~~~|              |!~~~~~~~'--------------'!!
__      !  |'.   !  |   '.!  |    |!  |    |!  |    |!  |    |!  |   .'!__|.'   !
~~ ________________________ !~~|                        |!~~|                        |!~~|                        |!~~|                        |!~~|                        |!~~|                        |!~~|                        |!~~|________________________|!
'. !  |!  |!  |!  |!  |!  |!  |!  |!.'

This encodes the left-side versions of the different screen components with ! instead of newlines, and ~ instead of ?, which are then right-padded with ~ to 30 characters before having themselves and their line-reversals added to a lookup list.
The rest of the code simply handles coordinate lookup with out-of-bounds cases ignored.

Python 2, 864 854 848 826 810 bytes
L=[zip(*[iter(w)]*30)for w in zip(*[iter("eJzdllESgyAMRL+5Rf7yRQ7AZbhIDl9BwTqzSVtHrbKffR0Mm13HEM5SFHIoadpNI3snDyaS6NCknhU+JfZOvq8kLoIBU1oEI+RTbiePGzBa3QM0rf78TGl17+CZr5ZrUXBN+ECfY1GvGKEqtDsSI4s6xTn5jgqyqNcTTnUjTQO2FAEqTC0ngCrtpywenX5le6or1SsGi9ZLBKt0HuXtVEeUNGdzG6EsRNmo2EzLxuBbqFH8njmfwnqGcl+VY+s5+5ezSYXVel4dxaRK/6F15SatK1frvm//y4aoT4Ckj6XWfY2cbvz2fLSCPiiVvR+3ZuerzDwPSqeSvgAP9woa".decode('base64').decode('zip'))]*300)]
E=enumerate
def f(m,x,y,d):
 D=eval(`[[' ']*30]*10`);w,h=len(m[0]),len(m);a=[m,zip(*m)[::-1]][d%2];x,y=[x,y,w+~x,h+~y,y,w+~x,h+~y,x][d::4];X=sum([[0]*(x<2)+list(l)[x-2+(x<2):x+3]for l in[l*2for l in[a,[l[::-1]for l in a[::-1]]][d/2]*2][y:y+4]],[])
 for d,w in zip(L,'sropqmklhfgca'):
  for j,l in E(d):
   for i,q in E(l):
    if q*X[ord(w)%32]>=' ':D[j][i]=q
 for l in D:print''.join(l)

Try it online!

Charcoal, 500 332 bytes
Ｆ⁴≔⮌Ｅ§θ⁰⁺00⭆θ§μλθ≔Ｅ✂θ⊖ζ⁺⁶ζ¹✂ι⊖η⁺⁶η¹θＦε≔⮌Ｅ§θ⁰⭆θ§μλθＢ³⁰χ Ｆ²«‖ＦΦ⪪⟦“ ｜0⟧Ｐ+Ｎ？⟧‹Ｇ”³¦⁰”{➙d⊟ＥＸ⍘k↧D({Ｖt⍘gＲd◨ⅉ^δ#T；”³¦¹“ ¶↖+9Ｇ₂pＦ^c1e⌈¬；”³χω⁰χ”{➙∧⊟∧◨ηü∧↖z↨⁸\G'λＩ∧¡∕⪫θＪoΣ³⊖Ｉ⊟ζ⊙”²¦⁰”{➙∧⊟∧◨ηü∨§·◧﹪d‹⟲ ＯzºκＦⅉＲï⎇”²¦⁷ω⁰χ”{➙∧⊟≔⊘⬤|↔3Ｚθ✂≔÷t⍘ε✂↨≔⧴×ld≕≡⌕m⟧6ψ=Ｚ”⁰¦⁰”}∧80ＫυgCAêＪm⟦↘/§‖Ｃk⮌Ｃ₂¡μ↗Ｗ”⁰¦²ω⁰χ”{⊟∨·◧¤∨¶⧴⬤2ＧＬ▷⁸ê5Ｇψ”⁰¦⁰⟧³Ｉ§⭆θ⭆³§μ⎇ι⊕ξ⁻⁵ξλ«Ｊ⁻⊟κ×²⁹¬ι⊟κ⊟κ

Try it online! Link is to verbose version of code. Somewhat boring approach, I'm afraid; lots of printing of compressed string literals. Explanation:
Ｆ⁴≔⮌Ｅ§θ⁰⁺00⭆θ§μλθ

Pad the array with two extra 0s on each side.
≔Ｅ✂θ⊖ζ⁺⁶ζ¹✂ι⊖η⁺⁶η¹θ

Slice a 7x7 subsection of the array centred on the given coordinates.
Ｆε≔⮌Ｅ§θ⁰⭆θ§μλθ

Rotate the array as appropriate for the given direction.
Ｂ³⁰χ

(note trailing space) Draw an empty 30×10 box so that the output is always a consistent size.
Ｆ²«‖

Draw each half separately, reflecting in between.
ＦΦ⪪⟦...⟧³Ｉ§⭆θ⭆³§μ⎇ι⁻⁵ξ⊕ξλ«

Take an array of wall descriptors, split into chunks of (string, y-coordinate, x-coordinate), filter on those chunks that have a wall at the relevant position in the relevant half of the array, and loop over the walls. The position is calculated by extracting 12 walls from the array and indexing them using the chunk index as this is golfier than locating the wall directly using the chunk index.
Ｊ⁻⊟κ×²⁹¬ι⊟κ⊟κ

Jump to the coordinates of the wall and print it. Note that reflecting negates the X-coordinates from [0, 30) to (-30, 0] so that on one pass the canvas is effectively shifted 29 characters to the left.

@Arnauld Indeed, I'm not taking any advantage of symmetry at all, I should be able to cut a third off by drawing each half separately.

+1 for a 168 byte golf stroke. I think that's the biggest single golf I have seen on here.

Ruby, 412 391 385 383 bytes
->a,x,y,d{b=Array.new(97){[" "]*10}
e=[-1,0,1,0]
14.times{|i|m=-i%3-1
w=[31,25,15,9][n=i>2?4-i/3:(m*=2;3)]
(a*3)[y+n*e[d]+m*c=e[d-3]][x+n*c-m*e[d]]&&[p=w*(m*2-1)/2,r=[12,7,4,3][n]*m*m.abs+m/3].min.upto([q=w*(m*2+1)/2,r].max){|j|t=" .'"*9
b[j+15]=(p<j&&j<q ?%w{-%2s- -%4s- _%7s_}[-n]%"":t[k=(j^j>>9)%(36/-~n)]+"   :|  |    |"[k%13]*(k/3*2)+t[-k]).center(10).chars}}
b[0,30].transpose}

Try it online!
Takes input as an array of truthy/falsy values (note 0 is truthy in Ruby, but nil is falsy.)
Outputs an array of characters.
Explanation
Blocks are drawn front to back with distance n decreasing and side to side position m cycling through -1,1,0 left,right,middle. The middle block E in the furthest row is actually drawn twice because we need to check both blocks A/B and blocks C/D. n,m and d are used to modify the x and y values to search array a. If x is out of range nil is returned for an out of range cell and no error is thrown, but if y is out of range nil will be returned for the row and Ruby will throw a type error when it tries to search for the cell. To avoid this the array is triplicated in the vertical direction prior to searching. If a truthy value is found a block is drawn.
The output is built in an array b of 10-element arrays representing the columns of the output, and is transposed into 10 rows at the end of the function. The full front face of all blocks is drawn (whether it appears in the viewport or not) so additional space is needed in the array to avoid out of range errors. The range of j values in the viewport is from -15 to +14, this is offset by 15 when saving to the array to give a range of 0 to 29. For each block drawn three values are calculated: p and q for the left and right corners of the front wall respectively, and r for the back of the side wall. j is iterated from the minimum to the maximum of these three values drawing the columns in turn.
There are 3 types of lines: horizontal - or _, vertical | or :, and diagonal with a repeating " .'" pattern. Where p < j < q columns containing spaces capped with - or _ are drawn to form the front face. Where j is outside this range, columns containing space, | or : are drawn capped with symbols from t=" .'" to form the edges and/or side face. This is managed by variable k=j where j is positive or k=-j-1 where j is negative. The number of characters between the top and bottom caps is k/3*2. In order to correctly handle the outer edges of the furthest blocks where n=3, k must be taken modulo 9, but this must not be done for smaller values of n. k is therefore taken modulo 36/-~n, where -~n evaluates to n+1.
Ungolfed code
->a,x,y,d{
  b=Array.new(97){[" "]*10}                                        #Set up array for output, allow space for plotting outside viewport
  e=[-1,0,1,0]                                                     #Direction offsets from player position
  14.times{|i|                                                     #Iterate through all blocks including block E twice
    m=-i%3-1                                                       #Cycle -1,1,0 = left, right, centre
    n=i>2?4-i/3:(m*=2;3)                                           #Distance n=4-i/3. But if i/3==0 n=3 and double m for blocks A,B
    w=[31,25,15,9][n]                                              #Width of front face of block
    r=[12,7,4,3][n]*m*m.abs+m/3                                    #Value of j for back edge of block. m/3 offsets by -1 when m negative
    (a*3)[y+n*e[d]+m*c=e[d-3]][x+n*c-m*e[d]]&&(                    #If a block is present at the location then
      [p=w*(m*2-1)/2,r].min.upto([q=w*(m*2+1)/2,r].max){|j|        #Calculate left and right edges of front of block p,q and iterate
        t=" .'"*9                                                  #t=character constant for diagonal lines
        k=(j^j>>9)%(36/-~n)                                        #k=j for positive j=distance from centre. For negative j, k=-1-j by XOR with j>>9=-1. If n=3 take modulo 9 for correct output of outer side of block.
        b[j+15]=(p<j&&j<q ?%w{-%2s- -%4s- _%7s_}[-n]%"":           #If j between p&q, draw horizontal lines separated by 2,4 or 7 spaces depending on value of n
        t[k]+"   :|  |    |"[k%13]*(k/3*2)+t[-k]).center(10).chars #else draw space or vertical line capped by diagonal markers
      }
    )
  }
b[0,30].transpose}                                                 #Truncate values outside viewport, transpose, and return value.

Nice answer! I like the way all lines are generated programmatically, including diagonal ones.