Source

TextRPG / textrpg.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
#!/usr/bin/env python
# encoding: utf-8

"""TextRPG - Simple TextRPG module built on top of the 1d6 RPG library.

Usage: 
    - ministory.py 
    Test the ministory (a testcase for the textrpg module)
    - textrpg.py 
    Start the internal test story of the textrpg. 

Plans: 
    - Simple to use functions in easy to read scriptfiles in the style of the ministory file. 
    - char.compete(other, skill_name) -> See who wins and by how much. 

Ideas: 
    - Lazy loading modules, to be able to print stuff at once without having to print before the imports.
    - Add getting experience for groups and show the chars together (only one experience header instead of one per char). 


Basic design principles for the scripting language: 
    
    - The action is character centered wherever possible and useful. 
       -> char.say(text) instead of dialog(char, text)
    
    - Anything which affects only one character or any interaction between only a few characters which is initiated by one of them gets called from the character via char.action(). 
       -> char.compete_skill(char2, skill_name) instead of competition_skill(char1, char2, skill_name)
    
    - Anything which affects the whole scene, or a whole group of not necessarily interacting characters gets called as basic function via action() or as class in its own right via class.action(). 
       -> save([char1, char2]) instead of char1.save() char2.save()
    
    - The seperate class way should only be chosen, if the class can feel like a character in its own right and needs seperate states which may or may not be persistent over subsequent runs. 
       -> For example AI.choose_the_way(players_answer) or music.action()
    
    - Data should be stored inside the chars wherever possible. If a script gets started with the same character again, the situation should resemble the previous one as much as possible, except where dictated otherwise by the story. 
       -> char.save() instead of 'on quit' store_char_data(char) + 'on start' load_char_data(char)
    
    - Actions should be written as verb_noun or simply verb. 
       -> char.say() and char.compete_skill() instead of char.text() and char.skill_compete()

The code for the TextRPG can be found at U{http://dreehg.org/ArneBab/textrpg}

"""

# Structure for the metadata inspired by Fufezan: 
# -> http://fufezan.net/python.php

__copyright__ = """ 
  rpg-1d6 - A general roleplaying backend. 
  -> http://rpg-1d6.sf.net
----------------------------------------------------------------- 
© Copyright by Arne Babenhauserheide, 2008  (arne_bab@web.de) 

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
  MA 02110-1301 USA

""" 
__version__   = '0.3' 
__author__    = 'Arne Babenhauserheide' 
# __date__      = '7th March 2007' 
__url__       = 'http://rpg-1d6.sf.net' 


print "...Loading rpg library..."

from textrpg_1d6.char import Char as ews_char

def _(text):
    '''String function to allow localizing later on. '''
    return str(text)

class Char(ews_char):
    def __init__(self, template=False, *args, **kwds): 
        super(Char, self).__init__(template=template, *args, **kwds)
        self.battle_diff_treshold = 4 #: The treshold of the char below which no hit is scored. 
    
    def battle_stats(self):
        """@return: A string with battle status information, formatted as battle stats"""
        scrib = _("---battle-stats for ") + _(self.name) + _("---")
        scrib += _('\nLife: ') + str(self.TP) + _(" of ") + str(self.bTP)
        scrib += _('\nWounds: ') + str(self.wounds[0]) + _(" - crippling wounds: ") + str(self.wounds[1])
        scrib += _("\nSkill: ") + str(self.attack)
        scrib += _("\nWeapon: ") + str(self.weapon)
        scrib += _("\nArmor: ") + str(self.armor)
        scrib += _("\n---/battle-stats---")
        return scrib
    def say(self, data): 
        data = _(data)
        for i in data.split("\n"): 
            if i.strip() != "": 
                diag(self.name + ': ' + i, localize=False)
            # If the string is empty, just add a blank line without the characters name. 
            else: diag("...", localize=False)
        
    def compete_skill(self, other, skill_name, self_mods=[], other_mods=[]): 
        """Compete with the other char in the specified skill. 
        
        @return If we won and by how much: (won, diff). Diff is always >=0. 
        """
        my_result = self.roll(self.get_effective_skill_value(skill_name, related_skills=[], related_attributes=[]), mods=self_mods)
        other_result = other.roll(other.get_effective_skill_value(skill_name, related_skills=[], related_attributes=[]), mods=other_mods)
        return my_result > other_result, abs(my_result - other_result)
    
    def fight_round(self, other, *args, **kwds): 
        """Fight for one round."""
        return super(Char, self).fight_round(other, *args, **kwds)
    
    def battle(self, other): 
        """Fight a dadly battle."""
        return battle(self, other)
    
    def get_exp(self, amount=0): 
        """Get experience and show it."""
        return give_exp(self, amount)
        
        
    def fight_round(self,  other, styles_self=[], styles_other=[]):
        """One battle action against another char.
        
    @param styles_self: The different styles the char should use (defense, pull back, escape, target: area). 
    @type styles_self: A list of Strings. 
    
    @param styles_other: The different styles the enemy uses. 
    @type styles_other: A list of Strings. 
    
    @return: [ If the char won (bool), [your new wounds, critical, taken tp damage], [the 
enemies new wounds, critical, taken tp damage] ]

    Plans: 
        - TODO: Add options for own fight style. 
        - TODO: Add options for other fight style. 
        
    Planned options for own fight style: 
        - defense (+6, no damage enemy)
        - pull back (+9, no damage enemy)
        - try to escape. 
        - target specific body area for additional damage. 
        - Shield/evasion (treshold for the enemies diff, below which no hit was archieved).
    
    Ideas: 
        - Define a damage multiplier, which shows how effective the damage type is against the specific enemy -> do that in the char itself. 
        - Define the effectivity of the armor against the weapon. 
        - Get the treshold directly from the char. 
."""
       # Initialize all variables empty
        won = None #: Did we win this round?
        mods_self = [] #: The modifiers for the protagonists attack roll. 
        mods_other = [] #: The modifiers for the enemies attack roll. 
        deep_wounds_self, deep_wounds_other, critical_wounds_self, critical_wounds_other = 0, 0, 0, 0
        
        
        # Apply the effects of the fighting styles. 
            # For the attacker
        # The defensive style gives 6 points bonus, but the other char won't take any damage. 
        if "defensive" in styles_self: 
            mods_self.append(+6)
        # Targetting the head gives 6 points malus, but increases the damage by 18 points. 
        if "target head" in styles_self: 
            mods_self.append(-6)
            
            # For the defender
        # The defensive style gives 6 points bonus, but the other char won't take any damage. 
        if "defensive" in styles_other: 
            mods_other.append(+6)
        # Targetting the head gives 6 points malus, but increases the damage by 18 points. 
        if "target head" in styles_other: 
            mods_other.append(-6)
        
        
        #: The damage we get
        self.damage_self = 0
        #: The damage the other gets
        self.damage_other = 0
        
        # Do the rolls for both fighters. 
        attack_self = self.attack_roll(mods=mods_self)
        attack_other = other.attack_roll(mods=mods_other)
        
        # If I throw higher
        if attack_self > attack_other: 
            won = True
            if not "defensive" in styles_self: 
                # Now check for damage (we hit)
                # The damage consists of several factors. 
                # First the difference between the attack rolls. 
                self.damage_other += attack_self - attack_other
                # Then the damage of our weapon. 
                self.damage_other += self.dam
                # And substracted the armor of the other. 
                self.damage_other -= other.arm
                
                # For specific body ares, the damage increases. 
                if "target head" in styles_self: 
                    self.damage_other += 18
        
                # Clean out negative damage. If the damage is below zero, the armor caught all damage. 
                if self.damage_other < 0: 
                    self.damage_other = 0
                
                # Now actually do the damage. This returns a tuple: (new deep wounds, new critical wounds)
                deep_wounds_other, critical_wounds_other = other.damage(tp=self.damage_other)
        
        # If the other rolls better
        elif attack_self < attack_other: 
            won = False
            # Check for our damage (the other hit)
            if not "defensive" in styles_other: 
                # The damage consists of several factors. 
                # First the difference between the attack rolls. 
                self.damage_self += attack_other - attack_self
                # Then the damage of our weapon. 
                self.damage_self += other.dam
                # And substracted the armor of the other. 
                self.damage_self -= self.arm
                
                # For specific body ares, the damage increases. 
                if "target head" in styles_other: 
                    self.damage_self += 18
    
                # Clean out negative damage. If the damage is below zero, the armor caught all damage. 
                if self.damage_self < 0: 
                    self.damage_self = 0
                
                # Now actually take the damage. This returns a tuple: (new deep wounds, new critical wounds)
                deep_wounds_self, critical_wounds_self = self.damage(tp=self.damage_self)

        # If we get the same result, the attacker wins, us. 
        else:   
            won = True
            if not "defensive" in styles_self: 
                # Now check for damage (we hit)
                # The damage consists of several factors. 
                # First the difference between the attack rolls. 
                self.damage_other += attack_self - attack_other
                # Then the damage of our weapon. 
                self.damage_other += self.dam
                # And substracted the armor of the other. 
                self.damage_other -= other.arm
                
                # For specific body ares, the damage increases. 
                if "target head" in styles_self: 
                    self.damage_other += 18
            
                # Clean out negative damage. If the damage is below zero, the armor caught all damage. 
                if self.damage_other < 0: 
                    self.damage_other = 0
                
                # Now actually do the damage. This returns a tuple: (new deep wounds, new critical wounds)
                deep_wounds_other, critical_wounds_other = other.damage(tp=self.damage_other)
            
        return won, [deep_wounds_self, critical_wounds_self, self.damage_self], [deep_wounds_other, critical_wounds_other, self.damage_other]
    

def ask(question): 
    """Ask a question."""
    return raw_input(_(question) + " ")

def story(data):
    data = _(data)
    for i in data.split("\n"): 
        if i.strip() != "": 
            diag(i, localize=False)
        # If the string is empty, just add a blank line without the characters name. 
        else: diag("...", localize=False)
	

def save(chars=[]):
        """Save the current state."""
        for i in chars:
                i.save()

def diag(data, localize=True):
	if localize: 
		raw_input(_(data))
	else: raw_input(data)

def greet(char): 
    diag("Old man: Welcome traveller. You've come to the right place to learn about your heritage.")
    diag("Sadly you aren' well liked around here.")
    diag("Heges for example didn't like your father too much.")
    diag("Oh well, that was an understatment. You should better prepare yourself to face him. I think he'd love to see your face in the mud. Here's my knife. I like fights to be fair.")
    diag("...")
    diag("You say you don't know who we think you are?")
    diag("Well, at least tell me your name then, and I'll tell you a bit about your father, after you won.")
    char.name = raw_input("My Name: ")
    diag("You've got a nice name, " + char.name + ". Good luck!")

def battle_info(me, other): 
    """Status information about a battle.
    
    @return: Completely formatted String to display. """
    return "\n" + me.battle_stats() + "\n\n" + other.battle_stats() + "\n"


def battle_round_result(me, other, won, injuries_self, injuries_other): 
    """Return the result of one round of battle. Called with the battle results (won, injuries_self, injuries_other)."""
    if won: 
        scribble = _("\nYou won this round.")
        if injuries_other[2] != 0: 
            scribble += "\n" + other.name + _(" took ") + str(injuries_other[2]) + _(" points of damage")
            if injuries_other[0] == 1: 
                scribble += _(" and a deep wound.") # You never get more than one wound per enemy in one round in the 1d6 rpg. 
            elif injuries_other[1] == 1: 
                scribble += _(" and a critical wound.") # You never get more than one wound per enemy in one round in the 1d6 rpg. 
            else: scribble += "."
    else: 
        scribble = _("\nYou didn't win this round.")
        if injuries_self[2] != 0: 
            scribble += "\n" + _("and you took ") + str(injuries_self[2]) + _(" points of damage")
            if injuries_self[0] == 1: 
                scribble += _(" and a deep wound.") # You never get more than one wound per enemy in one round in the 1d6 rpg. 
            elif injuries_self[1] == 1: 
                scribble += _(" and a critical wound.") # You never get more than one wound per enemy in one round in the 1d6 rpg. 
            else: scribble += "."
    return scribble


def select_battle_action(me, other, attack=True): 
    """Ask which action to take in the battle.
    
    @param me: The player char. 
    @type me: Char
    @param other: The enemy. 
    @type other: Char
    
    @param attack: Whether the character is the attacker. 
    @type attack: True/False
    
    @return: The result of the battle (won or lost, wounds)."""
    
    if ask('Do you want to attack ' + other.name + '? (Yes, no) ').lower() in ['yes', 'y', '']: 
        diag("You attack " + other.name + ".")
        won, injuries_self, injuries_other = select_battle_style(me, other, attacker=True)
    else:
        diag("You don't attack, so you could do something else this round, if this was already implemented.")
        won, injuries_self, injuries_other = False, [0, 0, 0], [0, 0, 0]
    return won, injuries_self, injuries_other 
    

def select_battle_style(me, other, attacker): 
    """Select how to fight.
    @param attacker: Is me the attacker? 
    @type: Bool
    """
    styles_self = []
    style = ask("How do you want to fight? (Usual, defensive, target head)")
    if style.lower() in ["target head", "head", "h"]: 
        styles_self.append("target head")
    elif style.lower() in ["defensive", "d"]: 
        styles_self.append("defensive")
    # TODO: Use the style. 
    if attacker: 
        won, injuries_self, injuries_other = me.fight_round(other, styles_self=styles_self)
    else: 
        won, injuries_other, injuries_self = other.fight_round(me, styles_other=styles_self)
        won = not won
    return won, injuries_self, injuries_other


def battle(me, other): 
    return fight_while_standing(me, other)

def fight_while_standing(me, other):
    '''Fight as long as both characters are still active.
    
    Plans: 
        - TODO: Select fight styles, when you attack as well as when the other attacks. 
    '''
    
    # Show the battle status of both chars. 
    diag(battle_info(me, other))
    
    # Ask the player which action to take.        
    diag(other.name + ' comes closer.')
    won, injuries_self, injuries_other = select_battle_action(me, other)
    
    diag(battle_round_result(me, other, won, injuries_self, injuries_other))
    
    diag(battle_info(me, other))
    
    while me.active and other.active:
        if won: # we won the last round, so we can attack again.
            won, injuries_self, injuries_other = select_battle_action(me, other)
        else: 
            diag(other.name + " attacks you.")
            won, injuries_self, injuries_other = select_battle_style(me, other, attacker=False)
        
        diag(battle_round_result(me, other, won, injuries_self, injuries_other))
        
        diag(battle_info(me, other))
    
    if me.active: 
        return True # We won
    else: 
        return False # We lost

def give_exp(char, amount=0): 
    """Give experience to only one char."""
    return get_experience([char], amount=amount)

def get_experience(chars, amount=0):
    """Show the experience the chars get and what they do with it."""
    # Dialog header
    if len(chars) == 1: 
        diag("\n***experience of " + chars[0].name + "***")
        diag(chars[0].name + " got " + str(amount) + " experience.")
    else: 
        diag("\n***experience***")
        diag("The characters got " + str(amount) + " experience.")
    
    # Info text for each char. 
    for i in chars: 
        # Upgrade in single point steps, so the experience gets spread a bit. 
        # if it's less than two points, use it completely. 
        if amount <= 2: 
            text = upgrade_info(i, amount)
            if text: 
                diag(text)
        # If it's more than one point, spread all but one in single points 
        # and at last improve by 1 plus the remaining fraction. 
        # With this, no value gets less than one point/Strich 
        # (no use in spreading points which can only give an increase by pure chance). 
        else: 
            for j in range(int(amount) -1): 
                text = upgrade_info(i, 1)
                if text: 
                    diag(text)
            text = upgrade_info(i, amount - int(amount) + 1)
            if text: 
                diag(text)
        
    # Dialog footer. 
    diag("***/experience***\n")

def upgrade_info(char, amount): 
    """Return the effects of improving teh char by the given amount of points/Strichen."""
    upgrade = char.upgrade(amount)
    item_name = " ".join(upgrade[0].items()[0][0])
    old_value = upgrade[0].items()[0][1]["Zahlenwert"] 
    new_value = upgrade[1].items()[0][1]["Zahlenwert"]
    if old_value < new_value:
        return char.name + " raised " + item_name + " from " + str(old_value) + " to " + str(new_value)
    else: 
        return False

if __name__ == "__main__": 
    print "...Creating main character..."
    char = Char()
    greet(char)
    print "...Creating enemy character..."
    choss = Char(source='tag:1w6.org,2008:Hegen')
    choss.name = "Hegen"
    won = fight_while_standing(char, choss)
    if won: 
        diag(char.name + " won the fight.")
    else: 
        diag(choss.name + " won the fight.")
    get_experience(char, 3)
    choss.upgrade(3)
    if won: 
        diag("Well done " + char.name + ". I knew my trust in you was well placed. Now about your father...")
    else: 
        diag("I didn't expect you to lose to him. Well, fate is a harsh teacher. Better luck next time!")