From 406b8f574c701e48e33167e6b30a16cd78b3b1c6 Mon Sep 17 00:00:00 2001 From: rootcoma Date: Sat, 2 Nov 2013 04:43:33 -0700 Subject: [PATCH 1/2] Add AutoVoice, Skip vote, fixed hints, default values changed to 15 seconds Adding AutoVoice skip vote Improved hints Removed bug when starting game after game stops because of no questions Changed default values in config.py 15 seconds per hint 15 seconds after question --- plugin.py | 183 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 34 deletions(-) diff --git a/plugin.py b/plugin.py index 54687ee..41696d9 100644 --- a/plugin.py +++ b/plugin.py @@ -1,3 +1,10 @@ +### +# Copyright (c) 2013, tann +# All rights reserved. +# +# +### + import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins @@ -41,7 +48,7 @@ class TriviaTime(callbacks.Plugin): self.storage.makeReportTable() #self.storage.dropQuestionTable() self.storage.makeQuestionTable() - self.storage.dropEditTable() + #self.storage.dropEditTable() self.storage.makeEditTable() #self.storage.insertUserLog('root', 1, 1, 10, 30, 2013) #self.storage.insertUser('root', 1, 1) @@ -63,6 +70,23 @@ class TriviaTime(callbacks.Plugin): # check the answer self.games[channel].checkAnswer(msg) + def doJoin(self,irc,msg): + username = str.lower(msg.nick) + print username + channel = str.lower(msg.args[0]) + user = self.storage.getUser(username) + print user + if len(user) >= 1: + if user[13] <= 10: + irc.sendMsg(ircmsgs.privmsg(channel, 'Giving VIP to %s for being top #%d this YEAR' % (username, user[13]))) + irc.queueMsg(ircmsgs.voice(channel, username)) + elif user[14] <= 10: + irc.sendMsg(ircmsgs.privmsg(channel, 'Giving VIP to %s for being top #%d this MONTH' % (username, user[14]))) + irc.queueMsg(ircmsgs.voice(channel, username)) + elif user[15] <= 10: + irc.sendMsg(ircmsgs.privmsg(channel, 'Giving VIP to %s for being top #%d this WEEK' % (username, user[15]))) + irc.queueMsg(ircmsgs.voice(channel, username)) + def addquestionfile(self, irc, msg, arg, filename): """[] Add a file of questions to the servers question database, filename defaults to configured quesiton file @@ -78,7 +102,6 @@ class TriviaTime(callbacks.Plugin): insertList = [] for line in filesLines: insertList.append((str(line).strip(),str(line).strip())) - info = self.storage.insertQuestionsBulk(insertList) irc.reply('Successfully added %d questions, skipped %d' % (info[0], info[1])) addquestionfile = wrap(addquestionfile, ['admin',optional('text')]) @@ -88,13 +111,13 @@ class TriviaTime(callbacks.Plugin): Get TriviaTime information, how many questions/users in database, time, etc """ infoText = '''\x0301,08 TriviaTime by #trivialand @ irc.freenode.net ''' - irc.queueMsg(ircmsgs.privmsg(msg.args[0], infoText)) + irc.sendMsg(ircmsgs.privmsg(msg.args[0], infoText)) infoText = '''\x0301,08 %d Users on scoreboard Time is %s ''' % (self.storage.getNumUser(),time.asctime(time.localtime())) - irc.queueMsg(ircmsgs.privmsg(msg.args[0], infoText)) + irc.sendMsg(ircmsgs.privmsg(msg.args[0], infoText)) numKaos = self.storage.getNumKAOS() numQuestionTotal = self.storage.getNumQuestions() infoText = '''\x0301,08 %d Questions and %d KAOS (%d Total) in the database ''' % ((numQuestionTotal-numKaos), numKaos, numQuestionTotal) - irc.queueMsg(ircmsgs.privmsg(msg.args[0], infoText)) + irc.sendMsg(ircmsgs.privmsg(msg.args[0], infoText)) info = wrap(info) def clearpoints(self, irc, msg, arg, username): @@ -115,7 +138,7 @@ class TriviaTime(callbacks.Plugin): topsText = '\x0301,08 TODAYS Top 10 - ' for i in range(len(tops)): topsText += '\x02\x0301,08 #%d:\x02 \x0300,04%s %d ' % ((i+1) , tops[i][1], tops[i][2]) - irc.queueMsg(ircmsgs.privmsg(channel, topsText)) + irc.sendMsg(ircmsgs.privmsg(channel, topsText)) irc.noReply() day = wrap(day) @@ -198,7 +221,7 @@ class TriviaTime(callbacks.Plugin): irc.error("You do not exist! There is no spoon.") else: infoText = '\x0305,08 %s\'s Stats:\x0301,08 Points (answers) \x0305,08Today: #%d %d (%d) This Week: #%d %d (%d) This Month: #%d %d (%d) This Year: #%d %d (%d)' % (msg.nick, info[16], info[10], info[11], info[15], info[8], info[9], info[14], info[6], info[7], info[13], info[4], info[5]) - irc.queueMsg(ircmsgs.privmsg(channel, infoText)) + irc.sendMsg(ircmsgs.privmsg(channel, infoText)) irc.noReply() me = wrap(me) @@ -216,12 +239,35 @@ class TriviaTime(callbacks.Plugin): Skip a question """ # is it a user? + """ try: user = ircdb.users.getUser(msg.prefix) # rootcoma!~rootcomaa@unaffiliated/rootcoma except KeyError: irc.error('You need to register with me to use this command. TODO: show command needed to register') return + """ + username = ircutils.toLower(msg.nick) channel = ircutils.toLower(msg.args[0]) + + timeSeconds = self.registryValue('skipActiveTime', channel) + totalActive = self.storage.getNumUserActiveIn(timeSeconds) + + if not self.storage.wasUserActiveIn(username, timeSeconds): + irc.error('Only users who have answered a question in the last 10 minutes can skip.') + return + + if username in self.games[channel].skipVoteCount: + irc.error('You can only vote to skip once.') + return + + self.games[channel].skipVoteCount[username] = 1 + + irc.sendMsg(ircmsgs.privmsg(channel, '%s voted to skip this question.' % username)) + + if (len(self.games[channel].skipVoteCount) / totalActive) < self.registryValue('skipThreshold', channel): + irc.noReply() + return + if channel not in self.games: irc.error('Trivia is not running.') return @@ -232,7 +278,7 @@ class TriviaTime(callbacks.Plugin): schedule.removeEvent('%s.trivia' % channel) except KeyError: pass - irc.queueMsg(ircmsgs.privmsg(channel, 'Skipped question!')) + irc.sendMsg(ircmsgs.privmsg(channel, 'Skipped question! (%d of %d voted)' % (len(self.games[channel].skipVoteCount), totalActive))) self.games[channel].nextQuestion() irc.noReply() skip = wrap(skip) @@ -247,7 +293,7 @@ class TriviaTime(callbacks.Plugin): irc.error("You do not exist! There is no spoon.") else: infoText = '\x0305,08 %s\'s Stats:\x0301,08 Points (answers) \x0305,08Today: #%d %d (%d) This Week: #%d %d (%d) This Month: #%d %d (%d) This Year: #%d %d (%d)' % (info[1], info[16], info[10], info[11], info[15], info[8], info[9], info[14], info[6], info[7], info[13], info[4], info[5]) - irc.queueMsg(ircmsgs.privmsg(channel, infoText)) + irc.sendMsg(ircmsgs.privmsg(channel, infoText)) irc.noReply() showstats = wrap(showstats,['nick']) @@ -330,10 +376,12 @@ class TriviaTime(callbacks.Plugin): schedule.removeEvent('%s.trivia' % channel) except KeyError: pass - irc.error(self.registryValue('alreadyStarted')) + #irc.error(self.registryValue('alreadyStarted')) + irc.sendMsg(ircmsgs.privmsg(channel, self.registryValue('starting'))) + self.games[channel] = self.Game(irc, channel, self) else: # create a new game - irc.queueMsg(ircmsgs.privmsg(channel, self.registryValue('starting'))) + irc.sendMsg(ircmsgs.privmsg(channel, self.registryValue('starting'))) self.games[channel] = self.Game(irc, channel, self) irc.noReply() start = wrap(start) @@ -359,7 +407,7 @@ class TriviaTime(callbacks.Plugin): self.games[channel].stop() else: del self.games[channel] - irc.queueMsg(ircmsgs.privmsg(channel, self.registryValue('stopped'))) + irc.sendMsg(ircmsgs.privmsg(channel, self.registryValue('stopped'))) irc.noReply() stop = wrap(stop) @@ -370,7 +418,7 @@ class TriviaTime(callbacks.Plugin): channel = ircutils.toLower(msg.args[0]) timeObject = time.asctime(time.localtime()) timeString = '\x0301,08The current server time appears to be %s' % timeObject - irc.queueMsg(ircmsgs.privmsg(channel, timeString)) + irc.sendMsg(ircmsgs.privmsg(channel, timeString)) irc.noReply() time = wrap(time) @@ -403,6 +451,7 @@ class TriviaTime(callbacks.Plugin): self.irc = irc # reset stats + self.skipVoteCount = {} self.streak = 0 self.lastWinner = '' self.hintsCounter = 0 @@ -527,7 +576,7 @@ class TriviaTime(callbacks.Plugin): Main game/question/hint loop called by event. Decides whether question or hint is needed. """ # out of hints to give? - if self.hintsCounter >= self.registryValue('maxHints', self.channel): + if self.hintsCounter >= 3: answer = '' # create a string to show answers missed for ans in self.answers: @@ -564,7 +613,9 @@ class TriviaTime(callbacks.Plugin): """ hintRatio = self.registryValue('hintShowRatio') # % to show each hint hints = '' - + ratio = float(hintRatio * .01) + charMask = self.registryValue('charMask', self.channel) + # create a string with hints for all of the answers for ans in self.answers: if str.lower(ans) in self.guessedAnswers: @@ -573,15 +624,39 @@ class TriviaTime(callbacks.Plugin): hints += ' ' if len(self.answers) > 1: hints += '[' - divider = int(len(ans) * self.hintsCounter * hintRatio / 100) - if divider == len(ans): - divider -= 1 - hints += ans[:divider] - masked = ans[divider:] - # unmasks a percentage of characters from the front of words - # TODO a better algorithm for hints - charMask = self.registryValue('charMask', self.channel) - hints += re.sub('\w', charMask, masked) + if self.hintsCounter == 0: + masked = ans + hints += re.sub('\w', charMask, masked) + elif self.hintsCounter == 1: + divider = int(len(ans) * ratio) + if divider > 3: + divider = 3 + if divider > len(ans): + divider = len(ans)-1 + hints += ans[:divider] + masked = ans[divider:] + hints += re.sub('\w', charMask, masked) + elif self.hintsCounter == 2: + divider = int(len(ans) * ratio) + if divider > 3: + divider = 3 + if divider > len(ans): + divider = len(ans)-1 + lettersInARow=divider + hints += ans[:divider] + ansend = ans[divider:] + hintsend = '' + unmasked = 0 + for i in range(len(ans)-divider): + masked = ansend[i] + if lettersInARow < 3 and unmasked < (len(ans)-divider+1) and random.randint(0,100) < hintRatio: + lettersInARow += 1 + hintsend += ansend[i] + unmasked += 1 + else: + lettersInARow=0 + hintsend += re.sub('\w', charMask, masked) + hints += hintsend if len(self.answers) > 1: hints += ']' #increment hints counter @@ -602,6 +677,7 @@ class TriviaTime(callbacks.Plugin): return # reset and increment + self.skipVoteCount = {} self.question = '' self.answers = [] self.alternativeAnswers = [] @@ -616,8 +692,9 @@ class TriviaTime(callbacks.Plugin): # grab the next q numQuestion = self.storage.getNumQuestions() if numQuestion == 0: - self.sendMessage('There are no questions. Stopping. If you are an admin use the addquestionfile to add questions to the database') self.stop() + self.sendMessage('There are no questions. Stopping. If you are an admin use the addquestionfile to add questions to the database') + return #print '%d questions' % numQuestion lineNumber = random.randint(1,numQuestion-1) @@ -742,11 +819,11 @@ class TriviaTime(callbacks.Plugin): """ # with colors? bgcolor? if color is None: - self.irc.queueMsg(ircmsgs.privmsg(self.channel, ' %s ' % msg)) + self.irc.sendMsg(ircmsgs.privmsg(self.channel, ' %s ' % msg)) elif bgcolor is None: - self.irc.queueMsg(ircmsgs.privmsg(self.channel, '\x03%02d %s ' % (color, msg))) + self.irc.sendMsg(ircmsgs.privmsg(self.channel, '\x03%02d %s ' % (color, msg))) else: - self.irc.queueMsg(ircmsgs.privmsg(self.channel, '\x03%02d,%02d %s ' % (color, bgcolor, msg))) + self.irc.sendMsg(ircmsgs.privmsg(self.channel, '\x03%02d,%02d %s ' % (color, bgcolor, msg))) def stop(self): """ @@ -773,18 +850,20 @@ class TriviaTime(callbacks.Plugin): # otherwise errors self.conn.text_factory = str - def insertUserLog(self, username, score, numAnswered, day=None, month=None, year=None): + def insertUserLog(self, username, score, numAnswered, day=None, month=None, year=None, epoch=None): if day == None and month == None and year == None: dateObject = datetime.date.today() day = dateObject.day month = dateObject.month year = dateObject.year + if epoch is None: + epoch = int(time.mktime(time.localtime())) if self.userLogExists(username, day, month, year): return self.updateUserLog(username, score, day, month, year) c = self.conn.cursor() username = str.lower(username) - c.execute('insert into triviauserlog values (NULL, ?, ?, ?, ?, ?, ?)', - (username, score, numAnswered, day, month, year)) + c.execute('insert into triviauserlog values (NULL, ?, ?, ?, ?, ?, ?, ?)', + (username, score, numAnswered, day, month, year, epoch)) self.conn.commit() c.close() @@ -902,7 +981,7 @@ class TriviaTime(callbacks.Plugin): return True return False - def updateUserLog(self, username, score, numAnswered, day=None, month=None, year=None): + def updateUserLog(self, username, score, numAnswered, day=None, month=None, year=None, epoch=None): username = str.lower(username) if not self.userExists(username): self.insertUser(username) @@ -911,6 +990,8 @@ class TriviaTime(callbacks.Plugin): day = dateObject.day month = dateObject.month year = dateObject.year + if epoch is None: + epoch = int(time.mktime(time.localtime())) if not self.userLogExists(username, day, month, year): return self.insertUserLog(username, score, numAnswered, day, month, year) c = self.conn.cursor() @@ -919,11 +1000,12 @@ class TriviaTime(callbacks.Plugin): numAns = numAnswered test = c.execute('''update triviauserlog set points_made = points_made+?, - num_answered = num_answered+? + num_answered = num_answered+?, + last_updated = ? where username=? and day=? and month=? - and year=?''', (scr,numAns,usr,day,month,year)) + and year=?''', (scr,numAns,epoch,usr,day,month,year)) self.conn.commit() c.close() @@ -1035,6 +1117,7 @@ class TriviaTime(callbacks.Plugin): day integer, month integer, year integer, + last_updated integer, unique(username, day, month, year) on conflict replace )''') except: @@ -1482,6 +1565,38 @@ class TriviaTime(callbacks.Plugin): c.close() return data + def getNumUserActiveIn(self,timeSeconds): + epoch = int(time.mktime(time.localtime())) + dateObject = datetime.date.today() + day = dateObject.day + month = dateObject.month + year = dateObject.year + c = self.conn.cursor() + result = c.execute('''select count(*) from triviauserlog + where day=? and month=? and year=? + and last_updated>?''', (day, month, year,(epoch-timeSeconds))) + rows = result.fetchone()[0] + c.close() + return rows + + def wasUserActiveIn(self,username,timeSeconds): + username = str.lower(username) + epoch = int(time.mktime(time.localtime())) + dateObject = datetime.date.today() + day = dateObject.day + month = dateObject.month + year = dateObject.year + c = self.conn.cursor() + result = c.execute('''select count(*) from triviauserlog + where day=? and month=? and year=? + and username=? and last_updated>?''', (day, month, year,username,(epoch-timeSeconds))) + rows = result.fetchone()[0] + c.close() + if rows > 0: + return True + return False + + def getQuestion(self, id): c = self.conn.cursor() c.execute('''select * from triviaquestion where id=?''', (id,)) From 43a5fa271cf0580f6dc768e800e5c22ed6935d30 Mon Sep 17 00:00:00 2001 From: rootcoma Date: Sat, 2 Nov 2013 04:44:23 -0700 Subject: [PATCH 2/2] Changing time between questions and hints add skip vote --- config.py | 153 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 74 deletions(-) diff --git a/config.py b/config.py index b25ea8c..e06d6cd 100644 --- a/config.py +++ b/config.py @@ -20,125 +20,130 @@ TriviaTime = conf.registerPlugin('TriviaTime') # CONFIGURATION # file locations for database and question conf.registerChannelValue(TriviaTime, 'sqlitedb', - registry.NormalizedString("""/tmp/trivia.t""", - """Location of sqlite database file""") - ) + registry.NormalizedString("""/tmp/trivia.t""", + """Location of sqlite database file""") + ) conf.registerChannelValue(TriviaTime, 'quizfile', - registry.NormalizedString("""/home/trivia/bogus.ques.sample""", - """Location of question file""") - ) + registry.NormalizedString("""/home/trivia/bogus.ques.sample""", + """Location of question file""") + ) # timeout, number of hints, values conf.registerChannelValue(TriviaTime, 'showPlayerStats', - registry.Boolean(True, - """Show player stats after correct answer?""") - ) + registry.Boolean(True, + """Show player stats after correct answer?""") + ) + +conf.registerChannelValue(TriviaTime, 'skipThreshold', + registry.Float(.5, + """Percentage of active players who need to vote to skip""") + ) + +conf.registerChannelValue(TriviaTime, 'skipActiveTime', + registry.Integer((10*60), + """Amount of time a user is considered active after answering a question""") + ) conf.registerChannelValue(TriviaTime, 'timeout', - registry.Integer(10, - """Time in between hints""") - ) + registry.Integer(15, + """Time in between hints""") + ) conf.registerChannelValue(TriviaTime, 'sleepTime', - registry.Integer(5, - """Time in between the end of one question and the start of another""") - ) + registry.Integer(15, + """Time in between the end of one question and the start of another""") + ) conf.registerChannelValue(TriviaTime, 'payoutKAOS', - registry.Integer(5000, - """Extra points for teamwork on KAOS""") - ) + registry.Integer(5000, + """Extra points for teamwork on KAOS""") + ) conf.registerChannelValue(TriviaTime, 'inactivityDelay', - registry.Integer(600, - """Time before game shuts off in seconds""") - ) + registry.Integer(600, + """Time before game shuts off in seconds""") + ) conf.registerChannelValue(TriviaTime, 'defaultPoints', - registry.Integer(750, - """Default points for a correct answer""") - ) - -conf.registerChannelValue(TriviaTime, 'maxHints', - registry.Integer(3, - """Number of hints given""") - ) + registry.Integer(750, + """Default points for a correct answer""") + ) conf.registerChannelValue(TriviaTime, 'hintShowRatio', - registry.Integer(35, - """Percent of word to show per hint""") - ) + registry.Integer(40, + """Percent of word to show per hint""") + ) conf.registerChannelValue(TriviaTime, 'charMask', - registry.NormalizedString('*', - """Masking character for hints""") - ) + registry.NormalizedString('*', + """Masking character for hints""") + ) # victory text, help messages, formatting conf.registerChannelValue(TriviaTime, 'starting', - registry.NormalizedString("""Trivia starting in 5 seconds, get ready!!!""", - """Message shown when trivia starts""") - ) + registry.NormalizedString("""Trivia starting in 5 seconds, get ready!!!""", + """Message shown when trivia starts""") + ) conf.registerChannelValue(TriviaTime, 'stopped', - registry.NormalizedString("""Trivia stopped. :'(""", - """Message shown when trivia stops""") - ) + registry.NormalizedString("""Trivia stopped. :'(""", + """Message shown when trivia stops""") + ) conf.registerChannelValue(TriviaTime, 'alreadyStopped', - registry.NormalizedString("""Trivia has already been stopped.""", - """Message stating chat has already been stopped""") - ) + registry.NormalizedString("""Trivia has already been stopped.""", + """Message stating chat has already been stopped""") + ) conf.registerChannelValue(TriviaTime, 'alreadyStarted', - registry.NormalizedString("""Trivia has already been started.""", - """Message stating chat has already been started""") - ) + registry.NormalizedString("""Trivia has already been started.""", + """Message stating chat has already been started""") + ) conf.registerChannelValue(TriviaTime, 'answeredKAOS', - registry.NormalizedString("""\x0304%s\x0312 gets \x0304%d\x0312 points for: \x0304%s\x0312""", - """Message for one correct guess during KAOS""") - ) + registry.NormalizedString("""\x0304%s\x0312 gets \x0304%d\x0312 points for: \x0304%s\x0312""", + """Message for one correct guess during KAOS""") + ) conf.registerChannelValue(TriviaTime, 'answeredNormal', - registry.NormalizedString("""\x0312DING DING DING, \x0304%s\x0312 got the answer -> \x0304%s\x0312 <- in \x0304%0.4f\x0312 seconds, and gets \x0304%d\x0312 points""", - """Message for a correct answer""") - ) + registry.NormalizedString("""\x0312DING DING DING, \x0304%s\x0312 got the answer -> \x0304%s\x0312 <- in \x0304%0.4f\x0312 seconds, and gets \x0304%d\x0312 points""", + """Message for a correct answer""") + ) conf.registerChannelValue(TriviaTime, 'notAnswered', - registry.NormalizedString("""\x0304Time's up!\x0312 The answer was -> \x0304%s\x0312 <-""", - """Message when no one guesses the answer""") - ) + registry.NormalizedString("""\x0304Time's up!\x0312 The answer was -> \x0304%s\x0312 <-""", + """Message when no one guesses the answer""") + ) conf.registerChannelValue(TriviaTime, 'notAnsweredKAOS', - registry.NormalizedString("""\x0312Time's up! \x0304No one got\x0312 %s""", - """Message when time is up and KAOS are left""") - ) + registry.NormalizedString("""\x0312Time's up! \x0304No one got\x0312 %s""", + """Message when time is up and KAOS are left""") + ) conf.registerChannelValue(TriviaTime, 'recapNotCompleteKAOS', - registry.NormalizedString("""\x0312Correctly Answered: \x0304%d of %d\x0312 Total Awarded: \x0304%d Points\x0312 to %d Players""", - """Message after KAOS game that not all questions were answered in""") - ) + registry.NormalizedString("""\x0312Correctly Answered: \x0304%d of %d\x0312 Total Awarded: \x0304%d Points\x0312 to %d Players""", + """Message after KAOS game that not all questions were answered in""") + ) conf.registerChannelValue(TriviaTime, 'recapCompleteKAOS', - registry.NormalizedString("""\x0312Total Awarded: \x0304%d Points\x0312 to %d Players""", - """Message after KAOS game that all questions were answered in""") - ) + registry.NormalizedString("""\x0312Total Awarded: \x0304%d Points\x0312 to %d Players""", + """Message after KAOS game that all questions were answered in""") + ) conf.registerChannelValue(TriviaTime, 'solvedAllKaos', - registry.NormalizedString("""Congratulations, \x0304You've Guessed Them All !!%s""", - """Message stating all KAOS have been guessed""") - ) + registry.NormalizedString("""Congratulations, \x0304You've Guessed Them All !!%s""", + """Message stating all KAOS have been guessed""") + ) conf.registerChannelValue(TriviaTime, 'bonusKAOS', - registry.NormalizedString(""" \x0300,04 Everyone gets a %d Point Bonus !!""", - """Message for bonus points from KAOS for group play""") - ) + registry.NormalizedString(""" \x0300,04 Everyone gets a %d Point Bonus !!""", + """Message for bonus points from KAOS for group play""") + ) conf.registerChannelValue(TriviaTime, 'playerStatsMsg', - registry.NormalizedString("""\x0304%s\x0312 has won \x0304%d\x0312 in a row! Total Points TODAY: \x0304%d\x0312 this WEEK \x0304%d\x0312 & this MONTH: \x0304%d\x0312""", - """Message showing a users stats after guessing a answer correctly""") - ) + registry.NormalizedString("""\x0304%s\x0312 has won \x0304%d\x0312 in a row! Total Points TODAY: \x0304%d\x0312 this WEEK \x0304%d\x0312 & this MONTH: \x0304%d\x0312""", + """Message showing a users stats after guessing a answer correctly""") + ) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: