How to Think Like a Programmer
The biggest problem people face with programming is that they watch too many tutorials. Don’t get me wrong — tutorials aren’t bad. But what happens is you copy and paste code, and at the end of the day you’ve learned nothing about how to structure or write something on your own.
I struggled with this myself. I learned GDScript from tutorials. I even learned how to make games in Godot. But when it was time to write code alone, I froze. I couldn’t structure anything. I didn’t understand why I needed multiple functions instead of one big block. I could write small fragments, but I didn’t truly understand what was behind all of it.
If you struggle like I did — let me show you how I figured it out.
It turns out there are really only two pillars to programming mastery:
- Thinking in Code — translating concepts into executable logic
- Writing Clean Code — structuring code for clarity and maintainability
Learn these two things and you can build anything. So let’s get into it.
Pillar 1: Thinking in Code
The Translation Problem
You understand “player damages enemy” as a concept. But how does that become code?
The trick is realizing that every programming language has more or less the same structure. The differences are mostly syntax. If you move from something like Python to C++, you get more features — but you can structure your code the same way with a few adjustments.
IMPORTANTFirst of all, you always — and I mean always — need to break code down to simple pseudocode. What does an if-statement actually do? What about a function? A for-loop? Here’s how I think about them:
if (something is true) → then do this; else do thatfunction (input/settings){ code that does something with your input and produces an output or changes some state}for (repeat until this condition is met){ code that runs a fixed number of times}while (this statement is true){ code that runs endlessly until the condition becomes false}CAUTIONPlease do NOT make a
while(true)loop unless you know what you’re doing — it can easily crash your computer.
Logical Operators
and, or, and not are essential. Let me demonstrate:
if this AND that→ both must be true for the whole condition to be trueif this OR that→ only one needs to be trueif NOT this→ reverses the statement. If it’s false, it becomes true. Think of it as a flipper.
For example, if FireRateLimited is a boolean that’s true when you can’t shoot, you want to shoot when it’s not limited:
while(!FireRateLimited){ shoot()}You can stack conditions:
while(!FireRateLimited and !MagazineEmpty){ shoot()}while(!FireRateLimited and !MagazineEmpty and Input.is_action_just_pressed("Shoot")){ shoot()}And so on. Once you internalize this, you have enough to build almost anything.
The Mental Model
Thinking in code means breaking down real-world concepts into five elements:
- Entities (nouns) — What objects exist? → Player, Enemy
- Properties (adjectives) — What describes them? → health, damage_value, is_alive
- Actions (verbs) — What can happen? → attack(), take_damage(), die()
- State Changes — How does the system evolve? → Enemy.health -= damage
- Conditions — When do things happen? → if enemy.health <= 0
For any concept you want to code, ask yourself:
- What are the nouns? → Classes/Objects
- What are the verbs? → Methods/Functions
- What are the relationships? → How objects interact
- What is the state? → What changes over time
- What is the flow? → Order of operations
Example: “Player Damages Enemy”
Let’s break it down:
- Who are the actors? → Player, Enemy
- What’s the action? → Dealing damage
- What changes? → Enemy’s health
- What follows? → Check if enemy dies
We need two files — one for the Hero, one for the Enemy.
Hero logic:
if (hero detects an enemy in range AND hero presses attack) then call Enemy.Damaged(10)Enemy logic:
int Health = 100
function Damaged(int damage){ Health -= damage}That’s it. Now you just need to research how to apply this in your specific engine or language.
Here’s how it looks in Godot:
Main Scene├── Player│ ├── Sprite2D│ └── RayCast2D└── Enemy ├── Sprite2D └── Collision2DPlayer.gd
@onready var ray_cast_2d: RayCast2D = $RayCast2D
func _process(delta: float) -> void: if ray_cast_2d.is_colliding(): var hit = ray_cast_2d.get_collider() hit.on_hit_by_raycast(10)Enemy.gd
var health := 100
func on_hit_by_raycast(damage: int) -> void: health -= damageConcept → pseudocode → real code. Every time.
More Examples
”Car Accelerates on a Highway”
Nouns: Car, Highway. Verbs: accelerate. Properties: speed, max_speed, position. Condition: can’t exceed max_speed.
class Car: def __init__(self): self.speed = 0 self.max_speed = 120
def accelerate(self, amount): self.speed = min(self.speed + amount, self.max_speed)”Trading Bot Buys When Price Drops Below Threshold”
Nouns: TradingBot, Order, Price. Verbs: monitor, execute. Condition: price < threshold AND not already in a position.
class TradingBot: def __init__(self, threshold): self.threshold = threshold self.in_position = False
def on_price_update(self, current_price): if current_price < self.threshold and not self.in_position: self.execute_buy(current_price)
def execute_buy(self, price): # execute order logic self.in_position = True”Inventory Removes Item When Player Uses It”
Nouns: Inventory, Item, Player. Verbs: use, remove, apply_effect. Flow: use item → apply effect → remove from inventory.
class Inventory: def __init__(self): self.items = {}
def use_item(self, item_name, player): if self.items.get(item_name, 0) <= 0: return False item = get_item_definition(item_name) item.apply_effect(player) self.items[item_name] -= 1 if self.items[item_name] <= 0: del self.items[item_name] return TrueSame pattern every time. Nouns, verbs, state, conditions, flow.
Pillar 2: Writing Clean Code
Thinking in code gets you working software. Writing clean code makes it maintainable software. Here’s the core principle:
IMPORTANTEvery function should do ONE thing, and its name should accurately describe that one thing.
The Problem: Hidden Responsibilities
# BAD — this function does TWO thingsdef check_password(password): if password == stored_password: initialize_session() # surprise side effect! return True return FalseThe name says “check” but it also initializes a session. You can’t reuse the check without triggering the session. You can’t test them independently.
# GOOD — each function does ONE thingdef check_password(password): return password == stored_password
def initialize_session(): # session logic here pass
# Usageif check_password(user_input): initialize_session()The 10 Principles of Clean Code
0. Naming Reveals Intent
Function names are verbs: calculate_damage(), validate_input(), spawn_enemy(). Variable names are descriptive: enemy_health not eh, player_position not pp. If a name requires a comment to explain, the name is wrong.
# BADdef calc(a, b): return a * b * 0.75
# GOODdef calculate_discounted_price(original_price, quantity): DISCOUNT_RATE = 0.75 return original_price * quantity * DISCOUNT_RATE1. Small Functions
If you can’t see the entire function on your screen, it’s probably too long. Aim for 10–20 lines max.
# BAD — god function doing everythingdef process_game_turn(): # 80 lines of player input, enemy AI, physics, rendering, scoring...
# GOOD — orchestrate focused functionsdef process_game_turn(): handle_player_input() update_enemy_ai() check_collisions() render_frame() update_score()2. Functions Do One Thing
If you need the word “and” to describe what a function does, it does too much. “Check password and initialize session” → split it.
3. Consistent Levels of Abstraction
High-level functions call mid-level functions. Mid-level calls low-level. Don’t mix raw database queries with business logic in the same function.
# BAD — mixed abstractiondef process_payment(order): total = order.total if order.user.credit_card[0:4] == "4532": charge_visa(total) db.execute("UPDATE orders SET status='paid' WHERE id=?", order.id)
# GOOD — each level reads clearlydef process_payment(order): validate_payment_method(order) charge_customer(order) mark_order_as_paid(order)4. Avoid Repeated Switch Statements
If you’re writing the same if/elif chain in multiple places, use polymorphism or data structures instead.
# BAD — duplicated logicdef calculate_damage(weapon_type, base_damage): if weapon_type == "sword": return base_damage * 1.2 elif weapon_type == "bow": return base_damage * 0.8
def get_attack_speed(weapon_type): if weapon_type == "sword": return 1.0 elif weapon_type == "bow": return 1.5
# GOOD — centralizedWEAPON_STATS = { "sword": {"damage_mult": 1.2, "speed": 1.0}, "bow": {"damage_mult": 0.8, "speed": 1.5},}5. Minimize Function Arguments
Zero to two arguments is ideal. Three or more? You probably need an object.
# BADdef create_character(name, health, mana, strength, dex, int, level, x, y, team): pass
# GOODdef create_character(name, stats, position, team, level=1): pass6. No Hidden Side Effects
A function called get_something() should never secretly modify state. If it changes things, make that obvious in the name.
# BAD — "check" but also modifiesdef check_password(password): if password == stored_password: self.session = initialize_session() # hidden! self.login_count += 1 # hidden! return True return False
# GOOD — explicitdef is_password_valid(password, stored_password): return password == stored_password7. Command-Query Separation
Functions should either do something (command) or answer something (query). Not both.
# BAD — does bothdef get_next_enemy(): enemy = self.enemies.pop() # changes state return enemy # returns data
# GOODdef has_next_enemy(): # query return len(self.enemies) > 0
def remove_next_enemy(): # command return self.enemies.pop()8. Handle Errors Properly
Don’t mix error handling with business logic. Use exceptions instead of error codes.
# GOODdef process_player_move(direction): validate_direction(direction) new_pos = calculate_new_position(direction) check_collision(new_pos) player.position = new_pos
try: process_player_move(user_input)except InvalidDirectionError: show_message("Invalid direction")except CollisionError as e: handle_collision(e)9. DRY — Don’t Repeat Yourself
If you’re copy-pasting code, extract it into a function.
# BAD — duplicateddef damage_player(amount): player.health -= amount if player.health <= 0: player.is_alive = False
def damage_enemy(enemy, amount): enemy.health -= amount if enemy.health <= 0: enemy.is_alive = False
# GOOD — shared logicdef apply_damage(entity, amount): entity.health -= amount if entity.health <= 0: entity.is_alive = False10. The Refinement Loop
Writing clean code is rewriting. First draft: make it work. Second pass: make it right. Third pass: make it fast — only if needed.
# First draft — just worksdef do_stuff(x, y): result = [] for i in x: if i > 5: result.append(i * y) return result
# Refined — clear and intentionaldef multiply_values_above_threshold(values, multiplier, threshold=5): return [v * multiplier for v in values if v > threshold]Real-World Example: Game Enemy AI
Let’s apply both pillars to a real scenario — an enemy AI system.
# BAD — god function that does everythingdef update_enemy(enemy, player, delta_time): # movement, attack, animation, health regen # all crammed into 40+ lines...# GOOD — each system separated
def get_movement_direction(enemy, player): if calculate_distance(enemy.position, player.position) < enemy.chase_range: return normalize(player.position - enemy.position) return enemy.patrol_direction
def move_enemy(enemy, direction, delta_time): enemy.position += direction * enemy.speed * delta_time
def can_attack(enemy, player): distance = calculate_distance(enemy.position, player.position) return distance < enemy.attack_range and enemy.attack_cooldown <= 0
def execute_attack(enemy, player): damage = calculate_damage(enemy.base_damage) apply_damage(player, damage) enemy.attack_cooldown = enemy.attack_delay
def update_animation(enemy): enemy.animation = "walk" if enemy.velocity.length() > 0 else "idle"
def regenerate_health(enemy, delta_time): if enemy.health < enemy.max_health: enemy.health = min(enemy.health + enemy.regen_rate * delta_time, enemy.max_health)
# Orchestrate — reads like a plandef update_enemy(enemy, player, delta_time): direction = get_movement_direction(enemy, player) move_enemy(enemy, direction, delta_time)
if can_attack(enemy, player): execute_attack(enemy, player)
update_animation(enemy) regenerate_health(enemy, delta_time)Each function is testable on its own. You can modify movement without touching combat. You can reuse apply_damage() for any entity. The orchestrating function reads like English.
Advanced Thinking Patterns
Once the fundamentals click, these patterns will level you up.
State Machine Thinking
When something has distinct modes — idle, walking, jumping, attacking — think in states and transitions. Each state knows its own behavior and when to hand off to the next.
class Player: def __init__(self): self.state = "idle"
def handle_input(self, input): if self.state == "idle": if input.jump: self.transition_to("jumping") elif input.move: self.transition_to("walking") elif input.attack: self.transition_to("attacking")Event-Driven Thinking
When one event should trigger many reactions — “enemy killed” updates score, plays sound, spawns loot — use an event bus. The publisher doesn’t know or care who’s listening.
events.subscribe("enemy_killed", score_system.add_points)events.subscribe("enemy_killed", audio_system.play_death_sound)events.subscribe("enemy_killed", loot_system.spawn_drops)
def kill_enemy(enemy): enemy.alive = False events.publish("enemy_killed", {"enemy": enemy, "position": enemy.position})Pipeline Thinking
When data flows through transformations — parse, validate, convert, filter, calculate — each stage is independent and composable.
def process_trading_data(raw_data): parsed = parse_csv(raw_data) cleaned = clean_trade_data(parsed) enriched = add_technical_indicators(cleaned) save_trades(enriched) return enrichedWant to process without saving? Just drop the last step. Want to add a new indicator? Insert one function. The pipeline doesn’t care.
Code Smells Cheat Sheet
Watch out for these red flags:
- Function name uses “and” →
validate_and_save()— split it - Generic names →
do_stuff(),handle(),manager()— rename or rethink - Long functions → 30+ lines usually means multiple responsibilities
- A “get” function that also modifies state → separate query from command
- Boolean flag arguments →
save(data, compress=True)— probably two functions - Copy-pasted blocks → extract into a shared function
- Global variables in calculations → pass them as arguments instead
- Can’t test without a database/API/filesystem → extract dependencies
The Mastery Loop
1. Encounter a concept ↓2. Think in code (break it down) ↓3. Write working code ↓4. Refactor for cleanliness ↓5. Review: Does it read well? Does each piece do one thing? ↓6. Learn from the pattern ↓ (Return to 1 with better instincts)Quick Reference
Thinking in Code — 5 Questions:
- What are the nouns? → Objects/data
- What are the verbs? → Actions/functions
- What is the state? → What changes
- What are the conditions? → When things happen
- What is the flow? → Order of operations
Clean Code — 5 Rules:
- One function = one job
- Name reveals intent
- No hidden side effects
- No “and” in function descriptions
- If you can’t see it all on screen, it’s too long
The Refactoring Workflow:
- Does it work? → Make it work first
- What does it do? → Name it clearly
- Does it do one thing? → Split if not
- Can I test it? → Extract dependencies
- Is it readable? → Simplify
IMPORTANTWhen you’re stuck or your code feels messy, ask yourself two questions:
- Can I explain this concept without code? (Thinking)
- Does each piece do ONE clear thing? (Cleanliness)
These two questions will guide you back to clarity every time.
“First, make it work. Then, make it right. Then, make it fast.” — Kent Beck
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler