def solve_marple_logic(clues):
    letters = "ABCDEFGHIJKLMNOPQRST"
    poss = {letter: set(range(5)) for letter in letters}
    
    rows = [
        list("ABCDE"),
        list("FGHIJ"),
        list("KLMNO"),
        list("PQRST")
    ]
    
    def update_same_col(a, b):
        common = poss[a] & poss[b]
        if not common:
            raise ValueError(f"Contradiction: {a} and {b} cannot be in the same column")
        changed = False
        if poss[a] != common:
            poss[a] = common
            changed = True
        if poss[b] != common:
            poss[b] = common
            changed = True
        return changed

    def update_left_of(a, b):
        max_b = max(poss[b])
        new_a = {col for col in poss[a] if col < max_b}
        if not new_a:
            raise ValueError(f"Contradiction: {a} cannot be left of {b}")
        changed = False
        if new_a != poss[a]:
            poss[a] = new_a
            changed = True
        
        min_a = min(poss[a])
        new_b = {col for col in poss[b] if col > min_a}
        if not new_b:
            raise ValueError(f"Contradiction: {b} cannot be right of {a}")
        if new_b != poss[b]:
            poss[b] = new_b
            changed = True
        return changed

    def update_next_to(a, b):
        new_a = set()
        for ca in poss[a]:
            if any(abs(ca - cb) == 1 for cb in poss[b]):
                new_a.add(ca)
        if not new_a:
            raise ValueError(f"Contradiction: {a} and {b} are not next to each other")
        changed = False
        if new_a != poss[a]:
            poss[a] = new_a
            changed = True
        
        new_b = set()
        for cb in poss[b]:
            if any(abs(ca - cb) == 1 for ca in poss[a]):
                new_b.add(cb)
        if not new_b:
            raise ValueError(f"Contradiction: {b} and {a} are not next to each other")
        if new_b != poss[b]:
            poss[b] = new_b
            changed = True
        return changed

    def update_between(A, B, C):
        new_B = set()
        for b in poss[B]:
            found = False
            for a in poss[A]:
                for c in poss[C]:
                    if (a < b < c) or (c < b < a):
                        found = True
                        break
                if found:
                    break
            if found:
                new_B.add(b)
        if not new_B:
            raise ValueError(f"Contradiction: {B} is not between {A} and {C}")
        print(new_B)
        changed = False
        if new_B != poss[B]:
            print("bbb", poss[B])
            poss[B] = new_B
            changed = True
        
        new_A = set()
        for a in poss[A]:
            found = False
            for b in poss[B]:
                for c in poss[C]:
                    if (a < b < c) or (c < b < a):
                        print(a)
                        found = True
                        break
                if found:
                    break
            if found:
                new_A.add(a)
        print(new_A)
        if not new_A:
            raise ValueError(f"Contradiction: {A} not valid for between with {B} and {C}")
        if new_A != poss[A]:
            poss[A] = new_A
            print("aaa", poss[A])
            changed = True
        
        new_C = set()
        for c in poss[C]:
            found = False
            for a in poss[A]:
                for b in poss[B]:
                    if (a < b < c) or (c < b < a):
                        found = True
                        break
                if found:
                    break
            if found:
                new_C.add(c)
        print(new_C)
        if not new_C:
            raise ValueError(f"Contradiction: {C} not valid for between with {A} and {B}")
        if new_C != poss[C]:
            poss[C] = new_C
            print("ccc", poss[C])
            changed = True
        
        return changed

    def update_row(row_letters):
        changed = False
        for letter in row_letters:
            if len(poss[letter]) == 1:
                col = next(iter(poss[letter]))
                for other in row_letters:
                    if other == letter:
                        continue
                    if col in poss[other]:
                        poss[other].remove(col)
                        changed = True
        
        for col in range(5):
            candidates = []
            for letter in row_letters:
                if col in poss[letter]:
                    candidates.append(letter)
            if len(candidates) == 1:
                letter = candidates[0]
                print("letter :", letter)
                if poss[letter] != {col}:
                    poss[letter] = {col}
                    changed = True
                    for other in row_letters:
                        if other == letter:
                            continue
                        if col in poss[other]:
                            poss[other].remove(col)
                            changed = True
        print("update row :", changed)
        return changed

    changed = True
    while changed:
        changed = False
        for clue in clues:
            print('\n')
            print(clue, changed)
            try:
                if '^' in clue:
                    s1, s3 = clue[0], clue[2]
                    if update_same_col(s1, s3):
                        print("1 true", clue)
                        changed = True
                elif '<' in clue:
                    s1, s3 = clue[0], clue[2]
                    if update_left_of(s1, s3):
                        print("2 true", clue)
                        changed = True
                else:
                    if clue[0] == clue[2] and clue[0] != clue[1]:
                        s1, s2 = clue[0], clue[1]
                        if update_next_to(s1, s2):
                            print("3 true", clue)
                            changed = True
                    else:
                        if len(set(clue)) == 3:
                            if update_between(clue[0], clue[1], clue[2]):
                                print("4 true", clue)
                                changed = True
                
                print('\n'.join(f"{k}: {v}" for k, v in poss.items()) + "\n~~~~~~~~~~~~~~~~~~\n")
            except ValueError as e:
                print(f"Error processing clue {clue}: {e}")
                return None
        
        print("update before :", changed)
        for row in rows:
            try:
                if update_row(row):
                    print( "After update row", '\n'.join(f"{k}: {v}" for k, v in poss.items()), "\n################")
                    changed = True
            except ValueError as e:
                print(f"Error in row {row}: {e}")
                return None

    grid = [['.' for _ in range(5)] for _ in range(4)]
    for i, row_letters in enumerate(rows):
        for letter in row_letters:
            if len(poss[letter]) == 1:
                col = next(iter(poss[letter]))
                grid[i][col] = letter
            else:
                print(f"Warning: {letter} has multiple possibilities: {poss[letter]}")
    
    return grid

# clues = ["MRT", "ABH", "LKO", "OKP", "JIM", "OPE", "GDO", "RAQ", "J^A", "M^P", "A<Q", "D<K", "OQO"]
clues = ["HJL","POA","DHJ","KMA","DRG","PHD","AMQ","H<F","M<K","F<E","M<I","T<E","CPC","SOS"]

result = solve_marple_logic(clues)
if result:
    for row in result:
        print(' '.join(row))

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: