Created
January 20, 2026 13:46
-
-
Save dmberry/b8481e6f58ea39969cf90fe40c1fb5da to your computer and use it in GitHub Desktop.
Visible Love Generator (after Strachey, 1952) by David M. Berry
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Visible Love Generator (after Strachey, 1952) | |
| Author: David M. Berry | |
| Date: 20 January 2025 | |
| Version: 1.3 | |
| A reimplementation that foregrounds the generator's status as literary theory | |
| made executable. The program displays the template structure with numbered | |
| blank slots, then populates each slot as you press Enter, allowing you to | |
| experience the combinatorial process as it unfolds. | |
| Run: python3 love_letter_generator.py | |
| """ | |
| import os | |
| from random import choice, random | |
| # Word lists derived from Roget's Thesaurus categories, as Strachey used. | |
| SALUTATIONS = ['DARLING', 'DEAREST', 'BELOVED', 'MY OWN', 'SWEETHEART', 'MY DEAR'] | |
| ADJECTIVES = [ | |
| 'tender', 'precious', 'wistful', 'passionate', 'anxious', 'avid', | |
| 'burning', 'sweet', 'devoted', 'cherished', 'fond', 'eager', | |
| 'ardent', 'fervent', 'sympathetic', 'loving', 'curious', 'breathless' | |
| ] | |
| NOUNS = [ | |
| 'heart', 'affection', 'love', 'longing', 'desire', 'devotion', | |
| 'sympathy', 'feeling', 'passion', 'wish', 'hope', 'soul', | |
| 'adoration', 'hunger', 'thirst', 'liking', 'fancy', 'yearning' | |
| ] | |
| VERBS = [ | |
| 'yearns for', 'clings to', 'sighs for', 'longs for', 'hopes for', | |
| 'burns for', 'thirsts for', 'hungers for', 'pines for', 'aches for', | |
| 'craves', 'treasures', 'cherishes', 'holds dear', 'prizes' | |
| ] | |
| ADVERBS = [ | |
| 'anxiously', 'tenderly', 'wistfully', 'passionately', 'curiously', | |
| 'ardently', 'fondly', 'devotedly', 'eagerly', 'beautifully' | |
| ] | |
| CLOSINGS = ['YOURS', 'YOURS ALWAYS', 'FOREVER YOURS', 'EVER YOURS', 'YOUR OWN'] | |
| def clear(): | |
| os.system('cls' if os.name == 'nt' else 'clear') | |
| def show(lines, prompt=''): | |
| """Display the letter and wait for Enter.""" | |
| clear() | |
| print('=' * 55) | |
| print(' VISIBLE LOVE GENERATOR (after Strachey, 1952)') | |
| print(' David M. Berry, 20 January 2025, v1.3') | |
| print('=' * 55) | |
| print() | |
| for line in lines: | |
| print(' ', line) | |
| print() | |
| print('-' * 55) | |
| if prompt: | |
| input(f' {prompt}') | |
| else: | |
| input(' [Enter to continue]') | |
| def maybe(lst): | |
| """50% chance to pick a word, otherwise empty.""" | |
| return choice(lst) if random() < 0.5 else '' | |
| def slot(n): | |
| """Return a numbered blank slot.""" | |
| return f'[{n}]___' | |
| def run(): | |
| # How many body sentences | |
| num_sentences = choice([3, 4]) | |
| # Pre-generate all words and track slot numbers | |
| slot_num = 1 | |
| slots = {} # slot_num -> word | |
| # Salutation slots | |
| slots[slot_num] = choice(SALUTATIONS) | |
| sal1_slot = slot_num | |
| slot_num += 1 | |
| slots[slot_num] = choice(SALUTATIONS) | |
| sal2_slot = slot_num | |
| slot_num += 1 | |
| # Sentence slots | |
| sentence_slots = [] # list of lists of (slot_num, word) for each sentence | |
| for _ in range(num_sentences): | |
| sent = [] | |
| # adj1 (optional) | |
| adj1 = maybe(ADJECTIVES) | |
| if adj1: | |
| slots[slot_num] = adj1 | |
| sent.append((slot_num, 'adj1')) | |
| slot_num += 1 | |
| # noun1 | |
| slots[slot_num] = choice(NOUNS) | |
| sent.append((slot_num, 'noun1')) | |
| slot_num += 1 | |
| # adv (optional) | |
| adv = choice(ADVERBS) if random() < 0.3 else '' | |
| if adv: | |
| slots[slot_num] = adv | |
| sent.append((slot_num, 'adv')) | |
| slot_num += 1 | |
| # verb | |
| slots[slot_num] = choice(VERBS) | |
| sent.append((slot_num, 'verb')) | |
| slot_num += 1 | |
| # adj2 (optional) | |
| adj2 = maybe(ADJECTIVES) | |
| if adj2: | |
| slots[slot_num] = adj2 | |
| sent.append((slot_num, 'adj2')) | |
| slot_num += 1 | |
| # noun2 | |
| slots[slot_num] = choice(NOUNS) | |
| sent.append((slot_num, 'noun2')) | |
| slot_num += 1 | |
| sentence_slots.append(sent) | |
| # Closing slots | |
| slots[slot_num] = choice(CLOSINGS) | |
| close1_slot = slot_num | |
| slot_num += 1 | |
| slots[slot_num] = choice(ADVERBS).upper() | |
| close2_slot = slot_num | |
| # Track which slots are filled | |
| filled = set() | |
| def render_slot(n): | |
| if n in filled: | |
| return slots[n] | |
| else: | |
| return f'[{n}]___' | |
| def render_salutation(): | |
| return f'{render_slot(sal1_slot)} {render_slot(sal2_slot)}' | |
| def render_sentence(sent_slots): | |
| parts = ['My'] | |
| for sn, stype in sent_slots: | |
| if stype == 'noun2': | |
| parts.append(render_slot(sn) + '.') | |
| elif stype == 'verb': | |
| parts.append(render_slot(sn)) | |
| parts.append('your') | |
| else: | |
| parts.append(render_slot(sn)) | |
| return ' '.join(parts) | |
| def render_closing(): | |
| return f'{render_slot(close1_slot)} {render_slot(close2_slot)}' | |
| def build_lines(): | |
| lines = [render_salutation(), ''] | |
| for sent in sentence_slots: | |
| lines.append(render_sentence(sent)) | |
| lines.extend(['', render_closing(), '', 'M. U. C.']) | |
| return lines | |
| # Show initial template | |
| show(build_lines(), 'Press Enter to begin filling slots...') | |
| # Fill salutation | |
| filled.add(sal1_slot) | |
| show(build_lines(), f'Slot {sal1_slot}: {slots[sal1_slot]}') | |
| filled.add(sal2_slot) | |
| show(build_lines(), f'Slot {sal2_slot}: {slots[sal2_slot]}') | |
| # Fill sentences word by word | |
| for i, sent in enumerate(sentence_slots): | |
| for sn, stype in sent: | |
| filled.add(sn) | |
| show(build_lines(), f'Slot {sn}: {slots[sn]}') | |
| # Fill closing | |
| filled.add(close1_slot) | |
| show(build_lines(), f'Slot {close1_slot}: {slots[close1_slot]}') | |
| filled.add(close2_slot) | |
| show(build_lines(), f'Slot {close2_slot}: {slots[close2_slot]}') | |
| # Final | |
| show(build_lines(), 'Letter complete. Enter for another, Ctrl+C to exit.') | |
| print('\n\n') | |
| run() | |
| if __name__ == '__main__': | |
| try: | |
| run() | |
| except KeyboardInterrupt: | |
| print('\n Goodbye.') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment