Saturday, December 6, 2025

The Dhaka Shadows (Young Adult Thriller Long Story by Tahsin)

The Dhaka Shadows


Prologue – The Cinema Hall

The neon lights of Dhaka’s old cinema hall flickered against the monsoon-soaked streets. Rickshaws rattled past, their bells clashing with the distant hum of traffic. The air smelled of roasted peanuts and damp earth.

Nila Rahman, a striking law student with sharp wit and restless curiosity, stepped out of the rickshaw. She adjusted her dupatta, clutching her notebook. She wasn’t here for the film—she was here to meet her friend, but fate had other plans.

A man stumbled out of the alley beside the cinema, clutching his chest. Blood seeped through his shirt. He collapsed at Nila’s feet.

Her scream caught in her throat. A shadow slipped into the crowd, vanishing as if swallowed by the city.

Before she could react, a hand gripped her arm.

  • Rehan: “Don’t. If they see you, you’re next.”

It was Rehan, her classmate—charming, mischievous, always ready with a joke. Tonight, his grin was gone. His eyes darted nervously.

Moments later, another figure appeared—Kabir Khan, a detective known for his magnetic presence. Tall, broad-shouldered, with piercing eyes and a voice that carried authority, Kabir seemed carved out of the city’s shadows. His trench coat was soaked, his gaze sharp. He scanned the crowd, then locked eyes with Nila.

  • Kabir: “You saw something. And now you’re part of this.”

Two men, two paths. Nila was caught in the shadows of Dhaka.


Chapter 1 – The First Threat

The next morning, Nila tried to convince herself it was a nightmare. But when she opened her door, a note lay on the floor:

“Stay silent, or you’ll drown in the Buriganga.”

Her hands trembled. She stuffed the note into her bag and rushed to class.

Rehan found her in the cafeteria.

  • Rehan: “You look like you saw a ghost.”
  • Nila: “More like a murderer.”
  • Rehan: “Relax. Maybe it’s just a prank. Dhaka’s full of drama.”
  • Nila: “Pranks don’t come with blood.”

Kabir appeared later, waiting outside her lecture hall. His voice was low, urgent, but his presence was commanding—his confidence made Nila’s pulse quicken.

  • Kabir: “The man you saw was a whistleblower. He was about to expose a crime syndicate. That’s why he was killed.”
  • Nila: “And why threaten me?”
  • Kabir: “Because you saw too much.”

Chapter 2 – Rooftop Confessions

Dhaka’s skyline glittered as Nila met Rehan at a rooftop cafĂ©. Lanterns swayed in the breeze, casting golden light.

Rehan teased her, trying to break her tension.

  • Rehan: “If you’re going to be hunted by criminals, at least let me buy you coffee first.”
  • Nila: “You think this is funny?”
  • Rehan: “No. But if I don’t laugh, I’ll panic.”

Their laughter softened into closeness. A sudden rain shower forced them under a canopy. Rehan leaned in, his lips brushing hers. The kiss was warm, playful, full of promise.

But later that night, Kabir appeared at her door. His eyes were stormy, his presence magnetic.

  • Kabir: “Rehan’s hiding something. Don’t trust him.”
  • Nila: “You sound jealous.”
  • Kabir: “I sound cautious. There’s a difference.”

Nila’s heart twisted. Could Kabir be right? Or was he just trying to pull her closer?


Chapter 3 – The Syndicate

That evening, Kabir appeared at her family’s gate. His tall frame and confident stride made him impossible to ignore.

  • Kabir: “We need to talk. Not here. Walk with me.”

They walked through the narrow lanes of Old Dhaka, the smell of fried jilapi and damp bricks filling the air.

Kabir spoke in low tones.

  • Kabir: “The man you saw die was a whistleblower. He had evidence against a syndicate run by a powerful politician. That syndicate controls smuggling, extortion, even murders. He was silenced before he could speak.”

Nila’s stomach tightened. “And I’m a witness.”

Kabir nodded. “Which makes you a target.”


The Anonymous Calls

That night, Nila’s phone rang. The number was unknown.

  • Voice: “You think you’re clever, law student? Stay out of this, or your family pays.”

Her younger brother, Arif, overheard and grabbed the phone.

  • Arif: “Who are you? Leave my sister alone!”

The line went dead. Nila hugged him tightly, her heart pounding.


The Shadow Outside

Two days later, as Nila returned from class, she noticed a man standing across the street. He didn’t move, didn’t speak, just watched.

She hurried inside, locking the door. Kabir arrived minutes later, scanning the street.

  • Kabir: “They’re watching you. That means they’re nervous. Good. Nervous people make mistakes.”
  • Nila: “But what if they make those mistakes on my family?”
  • Kabir: “I won’t let that happen.”

His confidence was magnetic, and for a moment, Nila believed him completely.


Rehan’s Promise

The next day, Rehan found Nila in the university courtyard. He carried two cups of tea, his grin trying to mask the tension.

  • Rehan: “If anyone touches you, they’ll have to deal with me first.”
  • Nila: “You think jokes will protect me?”
  • Rehan: “No. But maybe my charm will confuse them long enough for you to run.”

She laughed despite herself. His warmth was a relief, but Kabir’s warnings echoed in her mind.


Kabir’s Strategy

Later that evening, Kabir took Nila to a quiet rooftop overlooking the city. The call to prayer echoed in the distance.

  • Kabir: “Protection isn’t enough. We need evidence. The syndicate thrives because no one can prove their crimes. If we find what the whistleblower left behind, we can expose them.”
  • Nila: “And if we don’t?”
  • Kabir: “Then we fight shadows with shadows. But I don’t lose.”

His piercing gaze made Nila’s pulse quicken.


Cliffhanger

That night, Nila returned home to find her mother pale and trembling.

  • Mother: “Arif didn’t come home from school. He’s missing.”

Chapter 4 – The Betrayal

Nila discovered Rehan’s phone contained encrypted files linked to the syndicate. When she confronted him, he admitted:

  • Rehan: “They blackmailed me. I had no choice. But I swear, I’d never hurt you.”

Her trust shattered. Kabir was right. Yet her feelings for Rehan lingered.

That night, Kabir kissed her under the rain-soaked streets. It was fierce, desperate, full of unspoken promises. Nila felt torn—two men, two truths.


Chapter 5 – The Showdown

The abandoned warehouse loomed at the edge of Dhaka’s riverfront, its rusted iron doors creaking in the wind. The smell of oil and damp concrete filled the air. Nila’s heart pounded as Kabir led her inside, Rehan trailing close behind.

Suddenly, floodlights blazed to life. Shadows stretched across the cracked floor. Men in black jackets emerged from the corners, armed with pipes and knives.

  • Kabir (calm, magnetic): “Stay behind me.”
  • Rehan (grinning despite the danger): “Behind you? I’m not letting you hog all the heroics.”

The first wave of attackers charged. Kabir moved like a storm—swift, precise. He disarmed one man with a twist of the wrist, sending his knife clattering across the floor. His tall frame and commanding presence made him seem untouchable.

Rehan, meanwhile, fought with reckless energy. He swung a broken plank, knocking two men back, laughing even as sweat dripped down his forehead.

  • Rehan: “Guess law school didn’t prepare us for this, huh, Nila?”
  • Nila (shouting): “Less jokes, more fighting!”

Nila grabbed a loose iron rod, surprising herself with her courage. She swung at an attacker who lunged too close, the clang echoing through the warehouse.


The Syndicate Leader Appears

From the shadows, the syndicate’s leader stepped forward—a tall man in a tailored suit, his smile cold.

  • Leader: “You should have stayed silent, Nila. Now you’ll drown like the rest.”

He gestured, and more men surged forward. Kabir fought with brutal efficiency, his trench coat whipping as he moved. Rehan shielded Nila, taking blows meant for her.


Betrayal and Sacrifice

In the chaos, Nila spotted Rehan’s phone slip from his pocket. The screen lit up with cartel codes. Her stomach dropped. Kabir had been right—Rehan was entangled.

But then Rehan shoved her out of the path of a blade, taking the cut across his arm instead.

  • Rehan (grimacing): “I may be a fool, but I’d never let them touch you.”

His sacrifice was real. Nila’s heart twisted—betrayal and love colliding in one moment.


Kabir’s Strategy

Kabir pulled Nila toward a stack of crates.

  • Kabir: “These shipments—they’re smuggling marine life. If we expose this, the syndicate falls.”

He smashed open a crate, revealing rare, glowing fish packed in water tanks. Nila snapped photos with her phone, her legal mind racing. Evidence. Proof.

The leader roared, charging at them. Kabir met him head‑on, fists flying. The fight was brutal—Kabir’s strength against the leader’s rage.


Climactic Battle

Rehan, bleeding but determined, tackled two men at once, giving Kabir the opening he needed. Kabir slammed the leader against the wall, pinning him.

  • Kabir (voice like steel): “Your shadows end tonight.”

Sirens wailed outside. Police lights flashed through the broken windows. The syndicate scattered, fleeing into the night.


Aftermath

Nila knelt beside Rehan, pressing her scarf against his wound.

  • Nila (tears in her eyes): “Why didn’t you tell me?”
  • Rehan (weak smile): “Because I wanted you to see me as more than my mistakes.”

Kabir crouched beside them, his hand steady on Nila’s shoulder. His presence was magnetic, grounding her in the chaos.

The police stormed in, arresting the remaining men. But the politician—the true mastermind—slipped away into the shadows, leaving the battle unfinished.

The syndicate cornered them in an abandoned warehouse. The air smelled of rust and damp concrete.

Nila, Rehan, and Kabir fought to survive.

Twists unfolded:

  • Rehan sacrificed himself to save Nila, proving his love.
  • Kabir nearly died protecting her, but together they outsmarted the syndicate using Nila’s legal knowledge and Kabir’s strategy.

The police arrived, but the politician slipped away into the shadows.


Chapter 6 – Dhaka at Dawn

At sunrise, Nila stood on a rooftop overlooking Dhaka. The city was alive, chaotic, beautiful.

Kabir stood beside her, tall and magnetic, his hand brushing hers. Their bond was undeniable, though the memory of Rehan lingered like a ghost.

Nila whispered:

“Dhaka hides its secrets in shadows. But sometimes, love is the only light.”

Streams into One River (Short Story)

Streams into One River

Opening

In the ancient city of Varanasi, where the Ganges shimmered under the moonlight and temple bells mingled with the call to prayer, four travelers met by chance in a quiet garden.

There was Father Thomas, a Christian priest from Kerala; Rabbi Eli, a Jewish teacher; Sheikh Rahman, a wandering Sufi mystic; and Swami Arjun, a Hindu monk from the Himalayas.

They sat beneath a banyan tree, each carrying the weight of their tradition. The air was thick with incense and curiosity.

Father Thomas broke the silence. “We all seek truth. But surely only one path leads to it.”

Rabbi Eli adjusted his shawl. “Truth is one, but hidden in layers. My path reveals one layer, yours another.”

Sheikh Rahman smiled faintly. “The Beloved is beyond names. Yet each name is a doorway.”

Swami Arjun closed his eyes. “The divine is like a jewel with many faces. Each face shines differently, but it is the same jewel.”

The debate had begun.


The Debate

They argued with passion, yet respect.

Father Thomas: “Christianity teaches salvation through Christ. Without Him, how can one reach God?”
Rabbi Eli: “Judaism teaches covenant and law, the living relationship with the Eternal. How can you claim your way is the only one?”
Sheikh Rahman: “Islam teaches surrender to the One, the unity of all creation. How can you deny the oneness of God?”
Swami Arjun: “Hinduism teaches liberation through self-realization, seeing the divine within. How can you deny the soul’s journey inward?”

The garden seemed to listen. Fireflies hovered like tiny lanterns, as if waiting for the answer.


The Turning Point

A child wandered into the garden, carrying four clay lamps. She placed them before the men and lit each one.

“Which lamp is brightest?” she asked.

The four leaned closer. Each lamp glowed equally, its flame steady.

Father Thomas whispered, “They shine the same.”
Rabbi Eli nodded. “Each flame is distinct, yet all are fire.”
Sheikh Rahman added, “The light is one, though the vessels differ.”
Swami Arjun smiled. “The child has answered for us.”


The Realization

They sat in silence, watching the lamps. Slowly, they understood:

  • Christianity’s love was a flame.
  • Judaism’s justice and covenant was a flame.
  • Islam’s unity and surrender was a flame.
  • Hinduism’s self-realization and liberation was a flame.

Each flame alone gave light, but together they illuminated the garden with brilliance.

Father Thomas said softly, “Perhaps truth is not a single road, but a river fed by many streams.”
Rabbi Eli replied, “Or a flame carried by many lamps.”
Sheikh Rahman whispered, “The Beloved wears many faces.”
Swami Arjun concluded, “Then the true path is not in choosing one, but in weaving them together.”


The Synthesis

Together, they wrote a simple teaching for the villagers:

  • Love without justice is incomplete.
  • Justice without compassion is harsh.
  • Unity without self-realization is blind.
  • Self-realization without service is empty.

But when love, justice, unity, and self-realization are woven together, they form a path that honors all four traditions.

The villagers called it The Garden of Four Lamps.


Ending

At dawn, the four men stood by the Ganges once more. The river shimmered, carrying countless reflections of the rising sun.

Father Thomas whispered, “I came to prove my path was the only one.”
Rabbi Eli smiled. “I came to defend mine.”
Sheikh Rahman bowed. “I came to sing of the One.”
Swami Arjun laughed softly. “And I came to walk mine alone.”

But now, they walked together, knowing that truth was not a single road, but a synthesis—the logical merging of faiths into one greater whole.

And so, in India, beneath the banyan tree and by the sacred river, a Christian father, a Jewish rabbi, a Muslim Sufi, and a Hindu monk discovered that the true path lay not in division, but in unity.

The Equations of Faith (Short Story by Tahsin)

The Equations of Faith

Opening

Dr. Cem Aksoy was known across Istanbul’s universities as the man who believed in nothing but numbers. An atheist, a physicist, and a relentless skeptic, Cem dismissed religion as “comfort for the mathematically untrained.” His lectures dazzled students with quantum mechanics, but his colleagues whispered that he lacked warmth, as if equations had replaced his heart.

One evening, while studying particle behavior in his lab overlooking the Bosphorus, Cem noticed something strange. His instruments recorded a pattern of oscillations that resembled… a cross. He laughed at the coincidence, but the image lingered in his mind.

That night, he dreamed of walking through Hagia Sophia. The mosaics glowed, and a voice whispered:

“You search for truth in particles. But truth is larger than particles.”

Cem awoke unsettled. For the first time, he wondered if his equations were pointing to something beyond physics.


The Turning Point

Curiosity gnawed at him. He began reading texts he had once mocked—Christian writings translated into Turkish, stories of saints, and the Gospel of John. To his surprise, the words resonated not as superstition but as poetry that hinted at a deeper order.

One passage struck him: “In the beginning was the Word, and the Word was with God, and the Word was God.”

Cem scribbled in his notebook: Word = Logos = Structure = Mathematics?

Could faith and science be describing the same framework in different languages?


The Embrace

Weeks later, Cem visited a small Christian community in Istanbul. He expected dogma, but instead found warmth, humility, and laughter. They spoke of love as a force stronger than gravity, forgiveness as a kind of entropy reversed.

During a quiet prayer, Cem felt something he had never experienced in a laboratory: peace. It was not measurable, yet undeniable. He whispered, “If this is faith, then I believe.”

From that day, Cem embraced Christianity—not as a rejection of science, but as its completion.


The Mission

But Cem was still a scientist. He could not abandon his quest for frameworks and equations. He began a new mission: to find a scientific language for spirituality.

He studied brain scans during prayer, measuring neural patterns. He analyzed the mathematics of hymns, the fractal geometry of stained glass, the physics of candlelight. He proposed that spirituality might be a resonance—a harmony between human consciousness and the universe’s hidden order.

His colleagues scoffed. “You’ve lost your mind,” they said.
Cem smiled. “Or perhaps I’ve found it.”


The Resolution

Years later, Cem published a book titled The Equations of Faith. It did not prove God with numbers, nor reduce prayer to formulas. Instead, it offered a bridge: a way to see science and spirituality as partners, not enemies.

In the final chapter, he wrote:

“Faith is not the absence of reason. It is the expansion of reason into realms where equations alone cannot reach.”


Ending

On a quiet evening, Cem stood again at the Bosphorus. The city lights shimmered like stars, and he whispered a prayer of gratitude. He was still a scientist, still a seeker of truth. But now, truth was not only in the particles—it was in the silence, the love, and the mystery that made life worth living.

And so, the Turkish atheist scientist became a theist, not by abandoning science, but by discovering that spirituality was the greatest equation of all.

The Last Message from Tomorrow (Tahsin's Science Fiction Short Story)


"A radio astronomer receives a signal from the year 2187. It’s a warning: “Do not activate the collider.” ....."

The Last Message from Tomorrow

Tehran, 2025

Dr. Laleh Farhadi adjusted the frequency dial on the rooftop radio array, her fingers trembling slightly in the cold morning air. The observatory’s antennas pointed toward the sky like ancient spears, waiting for whispers from the cosmos. She had spent the last seven years decoding signals from deep space, but this one was different.

It came at 2:18 a.m.—a burst of quantum noise, then a voice. Not alien. Not random. It was Persian.

“Do not activate the collider. You have 48 hours.”

The voice was female. Young. Familiar.

Laleh replayed the recording again and again. The timestamp embedded in the quantum signature read: 2187. She stared at the waveform, then at the encryption key. It was built on quantum entanglement protocols that hadn’t been invented yet.

She called her colleague, Dr. Kian Mahdavi, a theoretical physicist at Sharif University.

“Laleh,” he said, after listening to the message. “This is either the most elaborate hoax in history—or someone just sent a warning from the future.”

The Collider

The Iranian Quantum Collider, buried beneath the Zagros Mountains, was scheduled to activate in two days. It was designed to simulate conditions milliseconds after the Big Bang. Laleh had opposed it quietly for years, fearing the unknown. But now, the unknown had spoken.

She drove to the collider site in Kermanshah, passing checkpoints and military convoys. The project was national pride—proof that Iran could lead in quantum research. The director, Dr. Bahram Yazdi, greeted her with a smile that didn’t reach his eyes.

“You’re here to stop us?” he asked.

“I’m here to understand,” she replied.

She played the message. Bahram frowned. “A voice from the future? You expect me to believe this?”

“I don’t expect anything,” she said. “But the encryption uses entangled keys. Keys we haven’t developed yet. That’s not a prank.”

Bahram leaned back. “Even if it’s real, what does it mean? That the collider causes something catastrophic?”

Laleh hesitated. “Or maybe... it prevents something worse.”

The Family Connection

That night, Laleh sat in her apartment in Tehran, staring at the waveform analysis. On impulse, she ran a voice comparison against her own speech samples from lectures and interviews. The result stunned her: 87% match.

Her pulse quickened. The voice wasn’t her grandmother’s, nor a stranger’s—it was genetically and tonally close to her own. A descendant. Someone from her future family.

She called Kian again. “It’s not random. The voice matches me—almost perfectly.”

“You mean your daughter? Or granddaughter?” he asked.

“I don’t have children,” she said quietly. “But maybe I will. Maybe this warning is from someone who carries my voice into the future.”

Countdown

With 24 hours left, Laleh returned to the collider. She demanded a delay. Bahram refused.

“This is science, not superstition,” he said.

She broke protocol. She leaked the message to the press. Within hours, social media exploded. #MessageFromTomorrow trended across Iran. Protesters gathered outside the collider gates. The government issued a gag order.

But the pressure worked. The launch was postponed.

That night, Laleh received a second signal.

“You did it. The fracture is sealed. We’re still here.”

She wept.

Epilogue: Shiraz, 2187

A young woman named Roya Farhadi stood in a quiet lab beneath the ruins of Persepolis. She adjusted the transmitter, watching the quantum field stabilize.

Her ancestor had listened.

Time had bent, but not broken.

And the future—her future—was still possible.

Comic: “Young Adventurer Sintera and Princess Ariyana”

 






My Sister Ariyana's Lego Making Program

 

Features

  • Snap-to-grid canvas: Bricks align cleanly on a configurable grid.

  • Brick palette: Choose common sizes (1x1 to 6x2) and colors.

  • Drag & drop editing: Click a brick to select; drag to move; automatic collision feedback.

  • Rotate and delete: Use buttons or shortcuts (r, Delete).

  • Save/load projects: Persist your designs in JSON.

  • Export to PostScript: Create a printable vector snapshot of the canvas.

Source Code:

#!/usr/bin/env python3
# My sister Ariyana's lego making program
# Standard library + tkinter only

import json
import tkinter as tk
from tkinter import ttk, filedialog, messagebox

GRID_SIZE = 32          # pixels per grid cell
CANVAS_COLS = 24        # horizontal grid cells
CANVAS_ROWS = 16        # vertical grid cells
STUD_SIZE = 8           # diameter of studs in pixels
STUD_MARGIN = 6         # margin inside brick for studs

DEFAULT_COLOR = "#FF5757"

BRICK_SIZES = [
    ("1x1", 1, 1),
    ("2x1", 2, 1),
    ("3x1", 3, 1),
    ("4x1", 4, 1),
    ("2x2", 2, 2),
    ("3x2", 3, 2),
    ("4x2", 4, 2),
    ("6x2", 6, 2),
]

PALETTE_COLORS = [
    "#FF5757", "#FFA500", "#FFD700", "#32CD32",
    "#1E90FF", "#8A2BE2", "#A52A2A", "#808080",
    "#FFFFFF", "#000000"
]


class Brick:
    def __init__(self, gx, gy, w, h, color):
        # grid coordinates, width and height in grid cells
        self.gx = gx
        self.gy = gy
        self.w = w
        self.h = h
        self.color = color
        self.item_ids = []  # canvas item ids for the brick drawing
        self.tag = f"brick_{id(self)}"

    def bounds_px(self):
        x0 = self.gx * GRID_SIZE
        y0 = self.gy * GRID_SIZE
        x1 = (self.gx + self.w) * GRID_SIZE
        y1 = (self.gy + self.h) * GRID_SIZE
        return x0, y0, x1, y1

    def rotate(self):
        self.w, self.h = self.h, self.w


class LegoApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("My sister Ariyana's lego making program")

        # State
        self.bricks = []            # list[Brick]
        self.selected_brick = None
        self.drag_offset = (0, 0)   # offset in pixels during dragging
        self.current_size = (2, 2)  # default brick size (w,h)
        self.current_color = DEFAULT_COLOR

        self._build_ui()
        self._draw_grid()

        # Keyboard shortcuts
        self.bind("<Delete>", self.delete_selected)
        self.bind("<BackSpace>", self.delete_selected)
        self.bind("<r>", self.rotate_selected)
        self.bind("<R>", self.rotate_selected)

    def _build_ui(self):
        # Top menu
        menubar = tk.Menu(self)
        file_menu = tk.Menu(menubar, tearoff=False)
        file_menu.add_command(label="New", command=self.new_project)
        file_menu.add_command(label="Open…", command=self.load_project)
        file_menu.add_command(label="Save As…", command=self.save_project)
        file_menu.add_separator()
        file_menu.add_command(label="Export PostScript…", command=self.export_postscript)
        file_menu.add_separator()
        file_menu.add_command(label="Quit", command=self.quit)
        menubar.add_cascade(label="File", menu=file_menu)

        edit_menu = tk.Menu(menubar, tearoff=False)
        edit_menu.add_command(label="Rotate (r)", command=self.rotate_selected)
        edit_menu.add_command(label="Delete (Del)", command=self.delete_selected)
        menubar.add_cascade(label="Edit", menu=edit_menu)

        self.config(menu=menubar)

        # Layout: left palette, right canvas
        root_frame = ttk.Frame(self)
        root_frame.pack(fill="both", expand=True)

        palette = ttk.Frame(root_frame)
        palette.pack(side="left", fill="y", padx=8, pady=8)

        canvas_frame = ttk.Frame(root_frame)
        canvas_frame.pack(side="right", fill="both", expand=True, padx=8, pady=8)

        # Canvas
        width = CANVAS_COLS * GRID_SIZE
        height = CANVAS_ROWS * GRID_SIZE
        self.canvas = tk.Canvas(canvas_frame, width=width, height=height, bg="#f7f7f7", highlightthickness=1, highlightbackground="#ccc")
        self.canvas.pack(fill="both", expand=True)
        self.canvas.bind("<Button-1>", self.on_canvas_click)
        self.canvas.bind("<ButtonPress-1>", self.on_canvas_press)
        self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release)

        # Palette: sizes
        ttk.Label(palette, text="Brick sizes", font=("TkDefaultFont", 10, "bold")).pack(anchor="w", pady=(0, 4))
        size_frame = ttk.Frame(palette)
        size_frame.pack(fill="x", pady=(0, 8))

        for i, (label, w, h) in enumerate(BRICK_SIZES):
            btn = ttk.Button(size_frame, text=label, command=lambda W=w, H=h: self.set_size(W, H))
            btn.grid(row=i // 2, column=i % 2, sticky="ew", padx=2, pady=2)
        for c in range(2):
            size_frame.grid_columnconfigure(c, weight=1)

        # Palette: colors
        ttk.Label(palette, text="Colors", font=("TkDefaultFont", 10, "bold")).pack(anchor="w", pady=(8, 4))
        color_frame = ttk.Frame(palette)
        color_frame.pack(fill="x", pady=(0, 8))
        for i, col in enumerate(PALETTE_COLORS):
            btn = tk.Button(color_frame, bg=col, activebackground=col, width=3, command=lambda C=col: self.set_color(C))
            btn.grid(row=i // 5, column=i % 5, padx=2, pady=2, sticky="ew")
        for c in range(5):
            color_frame.grid_columnconfigure(c, weight=1)

        # Actions
        ttk.Label(palette, text="Actions", font=("TkDefaultFont", 10, "bold")).pack(anchor="w", pady=(8, 4))
        actions = ttk.Frame(palette)
        actions.pack(fill="x")
        ttk.Button(actions, text="Add brick", command=self.add_brick_from_palette).pack(fill="x", pady=2)
        ttk.Button(actions, text="Rotate (r)", command=self.rotate_selected).pack(fill="x", pady=2)
        ttk.Button(actions, text="Delete (Del)", command=self.delete_selected).pack(fill="x", pady=2)
        ttk.Separator(palette).pack(fill="x", pady=8)
        ttk.Label(palette, text="Tip: Click a brick to drag; r=rotate.", foreground="#555").pack(anchor="w")

        # Status bar
        self.status = tk.StringVar(value="Ready")
        status_bar = ttk.Label(self, textvariable=self.status, anchor="w")
        status_bar.pack(fill="x", side="bottom")

    def _draw_grid(self):
        self.canvas.delete("grid")
        w = CANVAS_COLS * GRID_SIZE
        h = CANVAS_ROWS * GRID_SIZE
        # Outer border
        self.canvas.create_rectangle(0, 0, w, h, outline="#bbb", width=1, tags=("grid",))
        # Grid lines
        for x in range(0, w, GRID_SIZE):
            self.canvas.create_line(x, 0, x, h, fill="#e3e3e3", tags=("grid",))
        for y in range(0, h, GRID_SIZE):
            self.canvas.create_line(0, y, w, y, fill="#e3e3e3", tags=("grid",))

    def set_size(self, w, h):
        self.current_size = (w, h)
        self.status.set(f"Selected size: {w}x{h}")

    def set_color(self, color):
        self.current_color = color
        self.status.set(f"Selected color: {color}")

    def add_brick_from_palette(self):
        # place at top-left by default, snap if occupied
        gx, gy = self._find_first_free_spot(self.current_size[0], self.current_size[1])
        brick = Brick(gx, gy, self.current_size[0], self.current_size[1], self.current_color)
        self.bricks.append(brick)
        self._render_brick(brick)
        self._select_brick(brick)
        self.status.set(f"Brick added at {gx},{gy}")

    def _find_first_free_spot(self, w, h):
        # naive scan; avoid overlapping existing bricks
        for gy in range(CANVAS_ROWS - h + 1):
            for gx in range(CANVAS_COLS - w + 1):
                if not self._occupied(gx, gy, w, h):
                    return gx, gy
        # fallback to 0,0 if full
        return 0, 0

    def _occupied(self, gx, gy, w, h, exclude_brick=None):
        for b in self.bricks:
            if b is exclude_brick:
                continue
            if (gx < b.gx + b.w and gx + w > b.gx and
                gy < b.gy + b.h and gy + h > b.gy):
                return True
        return False

    def _render_brick(self, brick):
        # Remove old drawing
        for iid in brick.item_ids:
            self.canvas.delete(iid)
        brick.item_ids = []

        x0, y0, x1, y1 = brick.bounds_px()
        # Base rectangle
        base = self.canvas.create_rectangle(
            x0+1, y0+1, x1-1, y1-1,
            fill=brick.color, outline="#333", width=2,
            tags=(brick.tag, "brick")
        )
        brick.item_ids.append(base)

        # Simple shadow
        shade = self.canvas.create_line(
            x0+2, y1-2, x1-2, y1-2,
            fill="#000000", width=1, stipple="gray50",
            tags=(brick.tag, "brick")
        )
        brick.item_ids.append(shade)

        # Studs grid: one stud per 1x1 cell inside the brick
        cols = max(1, brick.w)
        rows = max(1, brick.h)
        cell_w = (x1 - x0) / cols
        cell_h = (y1 - y0) / rows
        stud_d = min(STUD_SIZE, int(min(cell_w, cell_h) - STUD_MARGIN))
        stud_r = stud_d / 2
        for r in range(rows):
            for c in range(cols):
                sx = x0 + c * cell_w + cell_w / 2
                sy = y0 + r * cell_h + cell_h / 2
                stud = self.canvas.create_oval(
                    sx - stud_r, sy - stud_r, sx + stud_r, sy + stud_r,
                    fill=self._lighter(brick.color, 0.25),
                    outline="#444",
                    tags=(brick.tag, "brick")
                )
                brick.item_ids.append(stud)

        # Raise selection outline if selected
        if self.selected_brick is brick:
            self._draw_selection_outline(brick)

        # Bind clicks to select
        for iid in brick.item_ids:
            self.canvas.tag_bind(iid, "<Button-1>", self.on_brick_click)

    def _draw_selection_outline(self, brick):
        # Remove previous selection outlines
        self.canvas.delete("selection")
        x0, y0, x1, y1 = brick.bounds_px()
        outline = self.canvas.create_rectangle(
            x0+2, y0+2, x1-2, y1-2,
            outline="#00a3ff", width=2, dash=(4, 2),
            tags=("selection",)
        )
        return outline

    def _select_brick(self, brick):
        self.selected_brick = brick
        self._draw_selection_outline(brick)

    def on_brick_click(self, event):
        # Find brick via overlapping items
        items = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
        for iid in items:
            tags = self.canvas.gettags(iid)
            for t in tags:
                if t.startswith("brick_"):
                    # map tag to brick
                    for b in self.bricks:
                        if b.tag == t:
                            self._select_brick(b)
                            self.status.set("Brick selected")
                            return

    def on_canvas_click(self, event):
        # Clicking empty canvas clears selection
        items = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
        if not items:
            self.selected_brick = None
            self.canvas.delete("selection")
            self.status.set("No selection")

    def on_canvas_press(self, event):
        if self.selected_brick is None:
            return
        x0, y0, x1, y1 = self.selected_brick.bounds_px()
        if x0 <= event.x <= x1 and y0 <= event.y <= y1:
            self.drag_offset = (event.x - x0, event.y - y0)
        else:
            self.drag_offset = (0, 0)

    def on_canvas_drag(self, event):
        if self.selected_brick is None:
            return
        # Move brick visually during drag (not snapped yet)
        dx = event.x - self.drag_offset[0]
        dy = event.y - self.drag_offset[1]
        # Convert to grid position (float), then clamp to canvas
        gx = max(0, min(CANVAS_COLS - self.selected_brick.w, int(round(dx / GRID_SIZE))))
        gy = max(0, min(CANVAS_ROWS - self.selected_brick.h, int(round(dy / GRID_SIZE))))
        # Temporarily set position and render
        old_gx, old_gy = self.selected_brick.gx, self.selected_brick.gy
        self.selected_brick.gx, self.selected_brick.gy = gx, gy
        if self._occupied(gx, gy, self.selected_brick.w, self.selected_brick.h, exclude_brick=self.selected_brick):
            # show as red outline to indicate collision
            self._render_brick(self.selected_brick)
            self.canvas.delete("selection")
            x0, y0, x1, y1 = self.selected_brick.bounds_px()
            self.canvas.create_rectangle(x0+2, y0+2, x1-2, y1-2, outline="#ff0033", width=2, dash=(2, 2), tags=("selection",))
        else:
            self._render_brick(self.selected_brick)
        # Restore to be persistent on release
        self.selected_brick.gx, self.selected_brick.gy = old_gx, old_gy

    def on_canvas_release(self, event):
        if self.selected_brick is None:
            return
        # Snap to nearest grid
        gx = int(event.x // GRID_SIZE)
        gy = int(event.y // GRID_SIZE)
        gx = max(0, min(CANVAS_COLS - self.selected_brick.w, gx))
        gy = max(0, min(CANVAS_ROWS - self.selected_brick.h, gy))
        if not self._occupied(gx, gy, self.selected_brick.w, self.selected_brick.h, exclude_brick=self.selected_brick):
            self.selected_brick.gx = gx
            self.selected_brick.gy = gy
            self._render_brick(self.selected_brick)
            self._draw_selection_outline(self.selected_brick)
            self.status.set(f"Moved to {gx},{gy}")
        else:
            self.status.set("Cannot place: space occupied")

    def rotate_selected(self, event=None):
        b = self.selected_brick
        if not b:
            return
        # rotate if fits
        b.rotate()
        b.gx = min(b.gx, CANVAS_COLS - b.w)
        b.gy = min(b.gy, CANVAS_ROWS - b.h)
        if self._occupied(b.gx, b.gy, b.w, b.h, exclude_brick=b):
            b.rotate()  # revert
            self.status.set("Rotation blocked: collision")
            return
        self._render_brick(b)
        self._draw_selection_outline(b)
        self.status.set("Rotated")

    def delete_selected(self, event=None):
        b = self.selected_brick
        if not b:
            return
        for iid in b.item_ids:
            self.canvas.delete(iid)
        self.canvas.delete("selection")
        self.bricks.remove(b)
        self.selected_brick = None
        self.status.set("Brick deleted")

    def new_project(self):
        if not self._confirm_discard_changes():
            return
        for b in list(self.bricks):
            for iid in b.item_ids:
                self.canvas.delete(iid)
        self.canvas.delete("selection")
        self.bricks.clear()
        self.selected_brick = None
        self.status.set("New project")

    def save_project(self):
        data = {
            "version": 1,
            "grid_size": GRID_SIZE,
            "cols": CANVAS_COLS,
            "rows": CANVAS_ROWS,
            "bricks": [
                {"gx": b.gx, "gy": b.gy, "w": b.w, "h": b.h, "color": b.color}
                for b in self.bricks
            ]
        }
        path = filedialog.asksaveasfilename(
            title="Save Project",
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")]
        )
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2)
            self.status.set(f"Saved: {path}")
        except Exception as e:
            messagebox.showerror("Save Error", str(e))

    def load_project(self):
        if not self._confirm_discard_changes():
            return
        path = filedialog.askopenfilename(
            title="Open Project",
            filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")]
        )
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            self.bricks.clear()
            self.canvas.delete("all")
            self._draw_grid()
            for bdata in data.get("bricks", []):
                b = Brick(bdata["gx"], bdata["gy"], bdata["w"], bdata["h"], bdata["color"])
                self.bricks.append(b)
                self._render_brick(b)
            self.selected_brick = None
            self.status.set(f"Loaded: {path}")
        except Exception as e:
            messagebox.showerror("Load Error", str(e))

    def export_postscript(self):
        path = filedialog.asksaveasfilename(
            title="Export Canvas",
            defaultextension=".ps",
            filetypes=[("PostScript", "*.ps"), ("All Files", "*.*")]
        )
        if not path:
            return
        try:
            self.canvas.postscript(file=path, colormode="color")
            self.status.set(f"Exported: {path}")
        except Exception as e:
            messagebox.showerror("Export Error", str(e))

    def _lighter(self, hex_color, amount=0.2):
        # Simple lighten function (0..1)
        hex_color = hex_color.lstrip("#")
        r = int(hex_color[0:2], 16)
        g = int(hex_color[2:4], 16)
        b = int(hex_color[4:6], 16)
        r = int(r + (255 - r) * amount)
        g = int(g + (255 - g) * amount)
        b = int(b + (255 - b) * amount)
        return f"#{r:02X}{g:02X}{b:02X}"

    def _confirm_discard_changes(self):
        if not self.bricks:
            return True
        return messagebox.askyesno("Discard changes?", "This will clear the current canvas. Continue?")

def main():
    app = LegoApp()
    app.mainloop()

if __name__ == "__main__":
    main()

Tuesday, November 25, 2025

Tahsin's Python Chess Software with Tkinter and 4-ply Minimax (Alpha-beta)

Below is a single-file Python program using only the standard library and Tkinter. It supports legal move generation (including castling, en passant, and promotion), check detection, and an AI that searches 4 plies using minimax with alpha-beta pruning. White is human by default; black is AI.

Source Code:

import tkinter as tk

from tkinter import messagebox

import math

import random

import time


# Chess program with Tkinter GUI and 4-ply minimax (alpha-beta)

# Standard library only. Single file. Basic evaluation and full legal move generation.


# Board representation:

# 8x8 list of lists, each cell is a piece char or '.' for empty.

# White: 'P','N','B','R','Q','K'

# Black: 'p','n','b','r','q','k'


START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"


WHITE = 'w'

BLACK = 'b'


PIECE_VALUES = {

    'P': 100, 'N': 320, 'B': 330, 'R': 500, 'Q': 900, 'K': 20000,

    'p': -100, 'n': -320, 'b': -330, 'r': -500, 'q': -900, 'k': -20000

}


# Simple piece-square tables (midgame, from white's perspective)

# Values in centipawns; black uses mirrored indices

PST_PAWN = [

    0, 0, 0, 0, 0, 0, 0, 0,

    50, 50, 50, 50, 50, 50, 50, 50,

    10, 10, 20, 30, 30, 20, 10, 10,

    5, 5, 10, 25, 25, 10, 5, 5,

    0, 0, 0, 20, 20, 0, 0, 0,

    5,-5,-10, 0, 0,-10,-5, 5,

    5,10,10,-20,-20,10,10, 5,

    0, 0, 0, 0, 0, 0, 0, 0

]

PST_KNIGHT = [

    -50,-40,-30,-30,-30,-30,-40,-50,

    -40,-20, 0, 0, 0, 0,-20,-40,

    -30, 0,10,15,15,10, 0,-30,

    -30, 5,15,20,20,15, 5,-30,

    -30, 0,15,20,20,15, 0,-30,

    -30, 5,10,15,15,10, 5,-30,

    -40,-20, 0, 5, 5, 0,-20,-40,

    -50,-40,-30,-30,-30,-30,-40,-50

]

PST_BISHOP = [

    -20,-10,-10,-10,-10,-10,-10,-20,

    -10, 0, 0, 0, 0, 0, 0,-10,

    -10, 0, 5,10,10, 5, 0,-10,

    -10, 5, 5,10,10, 5, 5,-10,

    -10, 0,10,10,10,10, 0,-10,

    -10,10,10,10,10,10,10,-10,

    -10, 5, 0, 0, 0, 0, 5,-10,

    -20,-10,-10,-10,-10,-10,-10,-20

]

PST_ROOK = [

     0, 0, 0, 0, 0, 0, 0, 0,

     5,10,10,10,10,10,10, 5,

    -5, 0, 0, 0, 0, 0, 0,-5,

    -5, 0, 0, 0, 0, 0, 0,-5,

    -5, 0, 0, 0, 0, 0, 0,-5,

    -5, 0, 0, 0, 0, 0, 0,-5,

    -5, 0, 0, 0, 0, 0, 0,-5,

     0, 0, 0, 5, 5, 0, 0, 0

]

PST_QUEEN = [

    -20,-10,-10,-5,-5,-10,-10,-20,

    -10, 0, 0, 0, 0, 0, 0,-10,

    -10, 0, 5, 5, 5, 5, 0,-10,

     -5, 0, 5, 5, 5, 5, 0, -5,

      0, 0, 5, 5, 5, 5, 0, -5,

    -10, 5, 5, 5, 5, 5, 0,-10,

    -10, 0, 5, 0, 0, 0, 0,-10,

    -20,-10,-10,-5,-5,-10,-10,-20

]

PST_KING_MID = [

    -30,-40,-40,-50,-50,-40,-40,-30,

    -30,-40,-40,-50,-50,-40,-40,-30,

    -30,-40,-40,-50,-50,-40,-40,-30,

    -30,-40,-40,-50,-50,-40,-40,-30,

    -20,-30,-30,-40,-40,-30,-30,-20,

    -10,-20,-20,-20,-20,-20,-20,-10,

     20, 20, 0, 0, 0, 0, 20, 20,

     20, 30, 10, 0, 0, 10, 30, 20

]


UNICODE_MAP = {

    'P': '♙', 'N': '♘', 'B': '♗', 'R': '♖', 'Q': '♕', 'K': '♔',

    'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚'

}


SQUARE_SIZE = 64

BOARD_COLOR_LIGHT = "#F0D9B5"

BOARD_COLOR_DARK = "#B58863"

HIGHLIGHT_COLOR = "#FFD966"


class Position:

    def __init__(self, board, turn, castling, ep_target, halfmove, fullmove):

        self.board = board  # 8x8 list

        self.turn = turn    # 'w' or 'b'

        self.castling = castling  # string like "KQkq"

        self.ep_target = ep_target  # (r,c) or None

        self.halfmove = halfmove

        self.fullmove = fullmove


    def copy(self):

        return Position([row[:] for row in self.board], self.turn, self.castling, self.ep_target, self.halfmove, self.fullmove)


def parse_fen(fen):

    parts = fen.split()

    rows = parts[0].split('/')

    board = []

    for r in rows:

        row = []

        for ch in r:

            if ch.isdigit():

                for _ in range(int(ch)):

                    row.append('.')

            else:

                row.append(ch)

        board.append(row)

    turn = parts[1]

    castling = parts[2]

    ep = parts[3]

    ep_target = None

    if ep != '-':

        file = ord(ep[0]) - ord('a')

        rank = 8 - int(ep[1])

        ep_target = (rank, file)

    halfmove = int(parts[4]) if len(parts) > 4 else 0

    fullmove = int(parts[5]) if len(parts) > 5 else 1

    return Position(board, turn, castling, ep_target, halfmove, fullmove)


def to_fen(pos):

    rows = []

    for r in range(8):

        empty = 0

        row_str = ""

        for c in range(8):

            piece = pos.board[r][c]

            if piece == '.':

                empty += 1

            else:

                if empty:

                    row_str += str(empty)

                    empty = 0

                row_str += piece

        if empty:

            row_str += str(empty)

        rows.append(row_str)

    board_part = "/".join(rows)

    ep = '-'

    if pos.ep_target:

        ep = chr(pos.ep_target[1] + ord('a')) + str(8 - pos.ep_target[0])

    return f"{board_part} {pos.turn} {pos.castling if pos.castling else '-'} {ep} {pos.halfmove} {pos.fullmove}"


def in_bounds(r, c):

    return 0 <= r < 8 and 0 <= c < 8


def is_white(piece):

    return piece.isupper()


def is_black(piece):

    return piece.islower()


def side_of(piece):

    if piece == '.':

        return None

    return WHITE if is_white(piece) else BLACK


def opposite(side):

    return WHITE if side == BLACK else BLACK


def king_position(pos, side):

    target = 'K' if side == WHITE else 'k'

    for r in range(8):

        for c in range(8):

            if pos.board[r][c] == target:

                return (r, c)

    return None


def attacks_square(pos, r, c, side):

    # Check if side attacks (r,c). Used for check determination and castling.

    # Generate pseudo-legal attacks quickly.

    directions_bishop = [(-1,-1), (-1,1), (1,-1), (1,1)]

    directions_rook = [(-1,0), (1,0), (0,-1), (0,1)]

    directions_knight = [(-2,-1), (-2,1), (-1,-2), (-1,2), (1,-2), (1,2), (2,-1), (2,1)]

    directions_king = directions_bishop + directions_rook


    # Pawns

    if side == WHITE:

        for dc in (-1, 1):

            rr, cc = r+1, c+dc  # white pawns attack down from black perspective; but our board top is row 0 (rank 8). White pawns move towards decreasing row? Let's define:

    # Clarify: row 0 is top (rank 8), row 7 is bottom (rank 1). White pawns move from row 6->5->... upward (towards row 0).

    # So white pawn attacks (r-1, c±1); black pawn attacks (r+1, c±1).

    # Fix above:


    # White pawn attacks

    for dc in (-1, 1):

        rr, cc = r-1, c+dc

        if in_bounds(rr, cc) and pos.board[rr][cc] == 'P' and side_of('P') == side:

            return True

    # Black pawn attacks

    for dc in (-1, 1):

        rr, cc = r+1, c+dc

        if in_bounds(rr, cc) and pos.board[rr][cc] == 'p' and side_of('p') == side:

            return True


    # Knights

    for dr, dc in directions_knight:

        rr, cc = r+dr, c+dc

        if in_bounds(rr, cc):

            p = pos.board[rr][cc]

            if (p == 'N' and side == WHITE) or (p == 'n' and side == BLACK):

                return True


    # Bishops / Queens (diagonals)

    for dr, dc in directions_bishop:

        rr, cc = r+dr, c+dc

        while in_bounds(rr, cc):

            p = pos.board[rr][cc]

            if p != '.':

                if (side == WHITE and (p == 'B' or p == 'Q')) or (side == BLACK and (p == 'b' or p == 'q')):

                    return True

                break

            rr += dr

            cc += dc


    # Rooks / Queens (straight)

    for dr, dc in directions_rook:

        rr, cc = r+dr, c+dc

        while in_bounds(rr, cc):

            p = pos.board[rr][cc]

            if p != '.':

                if (side == WHITE and (p == 'R' or p == 'Q')) or (side == BLACK and (p == 'r' or p == 'q')):

                    return True

                break

            rr += dr

            cc += dc


    # Kings

    for dr, dc in directions_king:

        rr, cc = r+dr, c+dc

        if in_bounds(rr, cc):

            p = pos.board[rr][cc]

            if (p == 'K' and side == WHITE) or (p == 'k' and side == BLACK):

                return True


    return False


def is_in_check(pos, side):

    kpos = king_position(pos, side)

    if not kpos:

        return False

    return attacks_square(pos, kpos[0], kpos[1], opposite(side))


def generate_moves(pos):

    # Returns list of moves as tuples: ((r1,c1),(r2,c2),promotion_char_or_None,special)

    # special can be 'castle','enpassant', or None

    moves = []

    side = pos.turn

    forward = -1 if side == WHITE else 1

    start_rank = 6 if side == WHITE else 1

    promotion_rank = 0 if side == WHITE else 7


    for r in range(8):

        for c in range(8):

            p = pos.board[r][c]

            if p == '.':

                continue

            if side == WHITE and not is_white(p):

                continue

            if side == BLACK and not is_black(p):

                continue


            if p.upper() == 'P':

                # Forward moves

                rr = r + forward

                if in_bounds(rr, c) and pos.board[rr][c] == '.':

                    if rr == promotion_rank:

                        for promo in ('Q','R','B','N'):

                            moves.append(((r,c),(rr,c), promo if side==WHITE else promo.lower(), None))

                    else:

                        moves.append(((r,c),(rr,c), None, None))

                    # Double move

                    if r == start_rank:

                        rr2 = r + 2*forward

                        if in_bounds(rr2,c) and pos.board[rr2][c] == '.' and pos.board[rr][c] == '.':

                            moves.append(((r,c),(rr2,c), None, None))

                # Captures

                for dc in (-1,1):

                    rr = r + forward

                    cc = c + dc

                    if in_bounds(rr,cc):

                        target = pos.board[rr][cc]

                        if target != '.' and side_of(target) == opposite(side):

                            if rr == promotion_rank:

                                for promo in ('Q','R','B','N'):

                                    moves.append(((r,c),(rr,cc), promo if side==WHITE else promo.lower(), None))

                            else:

                                moves.append(((r,c),(rr,cc), None, None))

                # En passant

                if pos.ep_target:

                    er, ec = pos.ep_target

                    if er == r + forward and abs(ec - c) == 1:

                        if r == (3 if side == WHITE else 4):  # ep capture rank

                            moves.append(((r,c),(er,ec), None, 'enpassant'))


            elif p.upper() == 'N':

                for dr, dc in [(-2,-1),(-2,1),(-1,-2),(-1,2),(1,-2),(1,2),(2,-1),(2,1)]:

                    rr, cc = r+dr, c+dc

                    if in_bounds(rr,cc):

                        target = pos.board[rr][cc]

                        if target == '.' or side_of(target) == opposite(side):

                            moves.append(((r,c),(rr,cc), None, None))


            elif p.upper() == 'B':

                for dr, dc in [(-1,-1),(-1,1),(1,-1),(1,1)]:

                    rr, cc = r+dr, c+dc

                    while in_bounds(rr,cc):

                        target = pos.board[rr][cc]

                        if target == '.':

                            moves.append(((r,c),(rr,cc), None, None))

                        else:

                            if side_of(target) == opposite(side):

                                moves.append(((r,c),(rr,cc), None, None))

                            break

                        rr += dr

                        cc += dc


            elif p.upper() == 'R':

                for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:

                    rr, cc = r+dr, c+dc

                    while in_bounds(rr,cc):

                        target = pos.board[rr][cc]

                        if target == '.':

                            moves.append(((r,c),(rr,cc), None, None))

                        else:

                            if side_of(target) == opposite(side):

                                moves.append(((r,c),(rr,cc), None, None))

                            break

                        rr += dr

                        cc += dc


            elif p.upper() == 'Q':

                for dr, dc in [(-1,-1),(-1,1),(1,-1),(1,1),(-1,0),(1,0),(0,-1),(0,1)]:

                    rr, cc = r+dr, c+dc

                    while in_bounds(rr,cc):

                        target = pos.board[rr][cc]

                        if target == '.':

                            moves.append(((r,c),(rr,cc), None, None))

                        else:

                            if side_of(target) == opposite(side):

                                moves.append(((r,c),(rr,cc), None, None))

                            break

                        rr += dr

                        cc += dc


            elif p.upper() == 'K':

                for dr in (-1,0,1):

                    for dc in (-1,0,1):

                        if dr == 0 and dc == 0: continue

                        rr, cc = r+dr, c+dc

                        if in_bounds(rr,cc):

                            target = pos.board[rr][cc]

                            if target == '.' or side_of(target) == opposite(side):

                                moves.append(((r,c),(rr,cc), None, None))

                # Castling

                if side == WHITE and p == 'K' and 'K' in pos.castling:

                    # King side: e1->g1; squares f1,g1 empty; not in check on e1,f1,g1

                    if pos.board[7][5] == '.' and pos.board[7][6] == '.':

                        if not attacks_square(pos,7,4,BLACK) and not attacks_square(pos,7,5,BLACK) and not attacks_square(pos,7,6,BLACK):

                            moves.append(((7,4),(7,6), None, 'castle'))

                if side == WHITE and p == 'K' and 'Q' in pos.castling:

                    if pos.board[7][3] == '.' and pos.board[7][2] == '.' and pos.board[7][1] == '.':

                        if not attacks_square(pos,7,4,BLACK) and not attacks_square(pos,7,3,BLACK) and not attacks_square(pos,7,2,BLACK):

                            moves.append(((7,4),(7,2), None, 'castle'))

                if side == BLACK and p == 'k' and 'k' in pos.castling:

                    if pos.board[0][5] == '.' and pos.board[0][6] == '.':

                        if not attacks_square(pos,0,4,WHITE) and not attacks_square(pos,0,5,WHITE) and not attacks_square(pos,0,6,WHITE):

                            moves.append(((0,4),(0,6), None, 'castle'))

                if side == BLACK and p == 'k' and 'q' in pos.castling:

                    if pos.board[0][3] == '.' and pos.board[0][2] == '.' and pos.board[0][1] == '.':

                        if not attacks_square(pos,0,4,WHITE) and not attacks_square(pos,0,3,WHITE) and not attacks_square(pos,0,2,WHITE):

                            moves.append(((0,4),(0,2), None, 'castle'))

    # Filter for legal (king not in check after move)

    legal = []

    for mv in moves:

        npos = make_move(pos, mv)

        if not is_in_check(npos, side):

            legal.append(mv)

    return legal


def make_move(pos, move):

    # Move application returns a new Position

    (r1,c1), (r2,c2), promo, special = move

    side = pos.turn

    npos = pos.copy()


    piece = npos.board[r1][c1]

    target = npos.board[r2][c2]


    # Update halfmove clock

    if piece.upper() == 'P' or target != '.':

        npos.halfmove = 0

    else:

        npos.halfmove += 1


    # Clear en passant by default

    npos.ep_target = None


    # Move piece

    npos.board[r2][c2] = piece

    npos.board[r1][c1] = '.'


    # Special: en passant capture

    if special == 'enpassant':

        if side == WHITE:

            npos.board[r2+1][c2] = '.'

        else:

            npos.board[r2-1][c2] = '.'


    # Special: promotion

    if promo:

        npos.board[r2][c2] = promo


    # Special: castling move rook

    if special == 'castle':

        if side == WHITE:

            if c2 == 6:  # king side

                npos.board[7][5] = 'R'

                npos.board[7][7] = '.'

            else:        # queen side

                npos.board[7][3] = 'R'

                npos.board[7][0] = '.'

        else:

            if c2 == 6:

                npos.board[0][5] = 'r'

                npos.board[0][7] = '.'

            else:

                npos.board[0][3] = 'r'

                npos.board[0][0] = '.'


    # Set en passant target if double pawn push

    if piece.upper() == 'P' and abs(r2 - r1) == 2:

        ep_row = (r1 + r2) // 2

        npos.ep_target = (ep_row, c1)


    # Update castling rights

    def remove_castling(side_castles):

        npos.castling = ''.join(ch for ch in npos.castling if ch not in side_castles)


    # If king moves, remove that side's castling

    if piece == 'K':

        remove_castling('KQ')

    if piece == 'k':

        remove_castling('kq')

    # If rook moves or is captured, update

    if r1 == 7 and c1 == 7 and npos.board[7][7] != 'R':  # white h1 rook moved

        remove_castling('K')

    if r1 == 7 and c1 == 0 and npos.board[7][0] != 'R':  # white a1 rook moved

        remove_castling('Q')

    if r1 == 0 and c1 == 7 and npos.board[0][7] != 'r':  # black h8 rook moved

        remove_castling('k')

    if r1 == 0 and c1 == 0 and npos.board[0][0] != 'r':  # black a8 rook moved

        remove_castling('q')

    # If rook captured

    if r2 == 7 and c2 == 7 and target == 'R':

        remove_castling('K')

    if r2 == 7 and c2 == 0 and target == 'R':

        remove_castling('Q')

    if r2 == 0 and c2 == 7 and target == 'r':

        remove_castling('k')

    if r2 == 0 and c2 == 0 and target == 'r':

        remove_castling('q')


    # Switch turn

    npos.turn = opposite(pos.turn)

    if npos.turn == WHITE:

        npos.fullmove += 1


    return npos


def evaluate(pos):

    # Material + piece-square tables; perspective: White positive

    score = 0

    for r in range(8):

        for c in range(8):

            p = pos.board[r][c]

            if p == '.': continue

            score += PIECE_VALUES[p]

            idx_white = r*8 + c

            idx_black = (7-r)*8 + c  # mirror for black

            if p == 'P':

                score += PST_PAWN[idx_white]

            elif p == 'p':

                score -= PST_PAWN[idx_black]

            elif p == 'N':

                score += PST_KNIGHT[idx_white]

            elif p == 'n':

                score -= PST_KNIGHT[idx_black]

            elif p == 'B':

                score += PST_BISHOP[idx_white]

            elif p == 'b':

                score -= PST_BISHOP[idx_black]

            elif p == 'R':

                score += PST_ROOK[idx_white]

            elif p == 'r':

                score -= PST_ROOK[idx_black]

            elif p == 'Q':

                score += PST_QUEEN[idx_white]

            elif p == 'q':

                score -= PST_QUEEN[idx_black]

            elif p == 'K':

                score += PST_KING_MID[idx_white]

            elif p == 'k':

                score -= PST_KING_MID[idx_black]

    # Mobility

    legal = generate_moves(pos)

    mob = len(legal) if pos.turn == WHITE else -len(legal)

    score += 2 * mob

    return score


TT = {}  # Transposition table: key -> (depth, score, flag, best_move)

Z_KEYS = [[random.getrandbits(64) for _ in range(12)] for _ in range(64)]

Z_SIDE = random.getrandbits(64)

Z_CASTLE_KEYS = {ch: random.getrandbits(64) for ch in "KQkq"}

Z_EP_KEYS = [[random.getrandbits(64) for _ in range(8)] for _ in range(8)]


PIECE_TO_INDEX = {'P':0,'N':1,'B':2,'R':3,'Q':4,'K':5,'p':6,'n':7,'b':8,'r':9,'q':10,'k':11}


def zobrist_hash(pos):

    h = 0

    for r in range(8):

        for c in range(8):

            p = pos.board[r][c]

            if p != '.':

                h ^= Z_KEYS[r*8+c][PIECE_TO_INDEX[p]]

    if pos.turn == BLACK:

        h ^= Z_SIDE

    for ch in pos.castling:

        if ch in Z_CASTLE_KEYS:

            h ^= Z_CASTLE_KEYS[ch]

    if pos.ep_target:

        er, ec = pos.ep_target

        h ^= Z_EP_KEYS[er][ec]

    return h


def order_moves(pos, moves):

    # Simple move ordering: captures first, promotions next, then others; MVV-LVA

    def score_mv(mv):

        (r1,c1),(r2,c2),promo,special = mv

        target = pos.board[r2][c2]

        sc = 0

        if target != '.':

            sc += abs(PIECE_VALUES[target]) - abs(PIECE_VALUES[pos.board[r1][c1]])//10

        if promo:

            sc += 500

        if special == 'castle':

            sc += 50

        return -sc  # sort ascending then

    return sorted(moves, key=score_mv)


def minimax(pos, depth, alpha, beta):

    # Alpha-beta with transposition table

    key = zobrist_hash(pos)

    best_move = None

    if depth == 0:

        return evaluate(pos), None


    legal = generate_moves(pos)

    if not legal:

        # Checkmate or stalemate

        if is_in_check(pos, pos.turn):

            return (-999999 + (8-depth)) if pos.turn == WHITE else (999999 - (8-depth)), None

        else:

            return 0, None


    # TT lookup

    if key in TT:

        tt_depth, tt_score, tt_flag, tt_move = TT[key]

        if tt_depth >= depth:

            if tt_flag == 'EXACT':

                return tt_score, tt_move

            elif tt_flag == 'ALPHA' and tt_score <= alpha:

                return tt_score, tt_move

            elif tt_flag == 'BETA' and tt_score >= beta:

                return tt_score, tt_move

        if tt_move:

            # Move ordering: try stored best move first

            legal = [tt_move] + [m for m in legal if m != tt_move]


    legal = order_moves(pos, legal)


    if pos.turn == WHITE:

        value = -math.inf

        for mv in legal:

            child = make_move(pos, mv)

            sc, _ = minimax(child, depth-1, alpha, beta)

            if sc > value:

                value = sc

                best_move = mv

            alpha = max(alpha, value)

            if alpha >= beta:

                break

        flag = 'EXACT'

        if value <= alpha:

            flag = 'ALPHA'

        elif value >= beta:

            flag = 'BETA'

        TT[key] = (depth, value, flag, best_move)

        return value, best_move

    else:

        value = math.inf

        for mv in legal:

            child = make_move(pos, mv)

            sc, _ = minimax(child, depth-1, alpha, beta)

            if sc < value:

                value = sc

                best_move = mv

            beta = min(beta, value)

            if alpha >= beta:

                break

        flag = 'EXACT'

        if value <= alpha:

            flag = 'ALPHA'

        elif value >= beta:

            flag = 'BETA'

        TT[key] = (depth, value, flag, best_move)

        return value, best_move


class ChessGUI:

    def __init__(self, master):

        self.master = master

        master.title("Python Chess (8-ply AI)")


        self.canvas = tk.Canvas(master, width=8*SQUARE_SIZE, height=8*SQUARE_SIZE)

        self.canvas.pack()


        self.status = tk.StringVar()

        self.status_label = tk.Label(master, textvariable=self.status, font=("Arial", 12))

        self.status_label.pack(pady=4)


        self.canvas.bind("<Button-1>", self.on_click)


        self.pos = parse_fen(START_FEN)

        self.selected = None

        self.legal_moves_for_selected = []

        self.game_over = False

        self.ai_depth = 8  # fixed per request

        self.human_side = WHITE  # human plays white by default


        self.draw_board()

        self.update_status()


    def draw_board(self):

        self.canvas.delete("all")

        for r in range(8):

            for c in range(8):

                x1 = c*SQUARE_SIZE

                y1 = r*SQUARE_SIZE

                x2 = x1 + SQUARE_SIZE

                y2 = y1 + SQUARE_SIZE

                color = BOARD_COLOR_LIGHT if (r+c) % 2 == 0 else BOARD_COLOR_DARK

                self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="")

                if self.selected == (r, c):

                    self.canvas.create_rectangle(x1, y1, x2, y2, fill=HIGHLIGHT_COLOR, outline="")

                piece = self.pos.board[r][c]

                if piece != '.':

                    # Choose color for piece

                    fill = "#333" if is_black(piece) else "#EEE"

                    # Draw Unicode piece

                    self.canvas.create_text(x1+SQUARE_SIZE/2, y1+SQUARE_SIZE/2,

                                            text=UNICODE_MAP[piece], font=("Arial", 32), fill=fill)

        # Highlight legal targets for selected

        for mv in self.legal_moves_for_selected:

            (_, _), (r2,c2), _, _ = mv

            x1 = c2*SQUARE_SIZE

            y1 = r2*SQUARE_SIZE

            x2 = x1 + SQUARE_SIZE

            y2 = y1 + SQUARE_SIZE

            self.canvas.create_rectangle(x1, y1, x2, y2, outline="#FFD700", width=3)


    def on_click(self, event):

        if self.game_over:

            return

        c = event.x // SQUARE_SIZE

        r = event.y // SQUARE_SIZE

        if not in_bounds(r,c): return


        if self.pos.turn != self.human_side:

            return


        piece = self.pos.board[r][c]

        # If selecting a piece to move

        if self.selected is None:

            if piece != '.' and ((self.human_side == WHITE and is_white(piece)) or (self.human_side == BLACK and is_black(piece))):

                self.selected = (r,c)

                self.legal_moves_for_selected = [mv for mv in generate_moves(self.pos) if mv[0] == (r,c)]

            else:

                self.selected = None

                self.legal_moves_for_selected = []

        else:

            # Try to make a move to clicked square

            target_moves = [mv for mv in self.legal_moves_for_selected if mv[1] == (r,c)]

            if target_moves:

                # If multiple (promotion), pick queen by default; allow hold Shift for knight? Keep simple: pick first

                mv = target_moves[0]

                self.pos = make_move(self.pos, mv)

                self.selected = None

                self.legal_moves_for_selected = []

                self.draw_board()

                self.update_status()

                self.check_end()

                if not self.game_over:

                    self.master.after(50, self.ai_move)  # AI responds

            else:

                # Reselect if clicked own piece; otherwise clear

                if piece != '.' and ((self.human_side == WHITE and is_white(piece)) or (self.human_side == BLACK and is_black(piece))):

                    self.selected = (r,c)

                    self.legal_moves_for_selected = [mv for mv in generate_moves(self.pos) if mv[0] == (r,c)]

                else:

                    self.selected = None

                    self.legal_moves_for_selected = []

        self.draw_board()


    def ai_move(self):

        if self.game_over:

            return

        start = time.time()

        score, best = minimax(self.pos, self.ai_depth, -math.inf, math.inf)

        elapsed = time.time() - start

        if best is None:

            # No legal move

            self.check_end()

            return

        self.pos = make_move(self.pos, best)

        self.draw_board()

        self.status.set(f"AI moved. Eval: {score/100:.2f}. Time: {elapsed:.2f}s")

        self.check_end()


    def update_status(self):

        side = "White" if self.pos.turn == WHITE else "Black"

        self.status.set(f"Turn: {side} — FEN: {to_fen(self.pos)}")


    def check_end(self):

        legal = generate_moves(self.pos)

        if legal:

            return

        side = self.pos.turn

        if is_in_check(self.pos, side):

            self.game_over = True

            winner = "Black" if side == WHITE else "White"

            messagebox.showinfo("Game Over", f"Checkmate. {winner} wins.")

        else:

            self.game_over = True

            messagebox.showinfo("Game Over", "Stalemate.")


def main():

    root = tk.Tk()

    app = ChessGUI(root)

    root.mainloop()


if __name__ == "__main__":

    main()


Science and Engineering capabilities that currently belong mostly to Science Fiction

Below is a list of science and engineering capabilities that currently belong mostly to science fiction . Some have early experimental found...