Probability of defeating a dragon in one turn rolling a 20 sided die In math class we were asked an optional problem I can't solve on my own:

You are fighting a dragon with 250 hit points and are rolling a 20 sided die to deal damage. The dragon takes damage equal to the number rolled on the die with 2 exceptions: If you roll a 20 you deal no damage (dragon is immune to critical damage) and if you roll a 1 you break your combo and the dragon kills you immediatly. You roll until the dragon is defeated or you roll a 1 and break your combo. What is the probability of defeating the dragon?

My mathematical approaches included calculating the expected damage per roll $$ E_{ w }= \frac{ \sum_{ n=2 }^{ w-1 }{ n } }{ w } $$ where $$ E_{ 20 }= 9.45 $$ and then tried calculate the probability of not failing in the minumum number of rolls required to defeat the dragon but in the end that led me to a probability so low anyone would consider it 0 and didn't seemed right to begin with.
I did understand that the problem is a lot more complex than it seems, since the probability changes drastically with the number of rolls before any other roll and their results.
Through empirical testing with a small python program, the probability of winnng came out to be only about 25%, which surprised me, given the chance of dealing damage is 90%.
How would you correctly tackle the problem and even leave it "parameterized" so the HP of the dragon and number of sides on the die could be changed?

I think the most understandable method to compute probabilities like these is to define a Markov chain that represents all of the possible states that the game can be in, with absorbing states that represent the death of either the player or the dragon.
A Markov chain is a set of states together with an associated probability transition matrix, where the transition probabilities depend only on the current state. Storing all of the transition probabilities in a matrix is a convenient organizational tool for computing compound conditional probabilities like those you find in your game.
In this example, you can define a separate state for every possible value of damage that could have been dealt to the dragon (from 0 to 249). Additionally, you need two absorbing states to represent when either the player or the dragon dies. In an absorbing state, there is no escape. You have a 100% chance of remaining in that state for eternity.
Next, you define the probability transitions for each state. For example, if you have dealt 100 damage to the dragon, you have a 1/20 chance to go to the 102 damage state, 1/20 chance to go to the 103 damage state, etc up to 119. You also have a 1/20 chance to go to the 'player death' state, and a 1/20 chance to stay on state 100 (if you roll a 20). For another example, if you have dealt 249 damage, then you have a 1/20 chance of dying, a 1/20 chance of remaining at 249, and an 18/20 chance of defeating the dragon. These probabilities are all stored in a transition matrix $P$, such that $P_{ij} = $ the probability of moving from state $i$ to state $j$.
Finally, you define your initial state probability distribution vector (in this case we have a 100% chance of starting in the 0 damage state), and left-multiply this vector by your transition matrix raised the power of the number of times you repeat your attacks. This will output the exact probabilities that you will be in each state. In this game, there is a theoretical chance for the fight to last arbitrarily long if you continue rolling critical hits. One way to get around this possibility is to redefine the game to work on a 19 sided die and remove the critical strike option, since it is overall irrelevant to the final probabilities. Another option is to simply attack a huge number of times such that the chance the fight hasn't ended is essentially zero.
Since you mentioned Python, I've written up some functions for you that create the Markov transition matrix and compute the state probabilities.
import numpy as np

def markov_matrix(hp,num_sides):
    #P is the transition matrix.
    # Need one state for each hp of the dragon, plus a 'player dies' state and a 'dragon dies' state
    # States 0-hp are damage done states, state -2 is 'player dies', state -1 is 'dragon dies'
    P = []
    
    for damage_done in range(hp):
        P_row = np.zeros(hp+2)
        
        #For each state, the probability for the player to die is 1/num_sides.
        P_row[-2] = 1/num_sides
        
        #Otherwise, there is an equal probability to deal 2 or more damage.
        for hit_damage in range(2,num_sides):
            new_state_index = damage_done+hit_damage
            
            #If the total damage would be enough to kill the dragon, that probability is added to the 'dragon dies' state.
            if new_state_index >= hp:
                P_row[-1] = P_row[-1] + 1/num_sides
            else:
                P_row[new_state_index] = 1/num_sides
        
        #If a crit is rolled, the dragon is immune.
        P_row[damage_done] = 1/num_sides
        
        P.append(P_row)
    #The 'player dies' and 'dragon dies' states are absorbing.
    player_dies_row = np.zeros(hp+2)
    player_dies_row[-2] = 1
    P.append(player_dies_row)
    
    dragon_dies_row = np.zeros(hp+2)
    dragon_dies_row[-1] = 1
    P.append(dragon_dies_row)
    
    P = np.stack(P)
    return P

def probability_of_dragon_death(hp,num_sides,max_num_attacks):
    P = markov_matrix(hp,num_sides)
    initial_state_probabilities = np.zeros(hp+2)
    
    #Initially we are in the 0 damage done state with 100% probability.
    initial_state_probabilities[0] = 1
    
    final_state_probabilities = initial_state_probabilities@(np.linalg.matrix_power(P,max_num_attacks))
    return final_state_probabilities[-1]

Running the function probability_of_dragon_death(hp=250,num_sides=20,max_num_attacks=10000) returns a probability of $\approx 0.269880$.
For fun, I've also attached a figure showing your probability to defeat the dragon when given its HP.

