# Constellations Flash-Cards # # Copyright (c) 2010 by David A. Wallace # Copyright (c) 2012 Aneesh Dogra # # 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 2 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 # # CITATIONS: # # Bright Star Catalog # http://heasarc.gsfc.nasa.gov/W3Browse/star-catalog/bsc5p.html # # Constellation figures -- derived from charts at # http://www.astro.wisc.edu/~dolan/constellations/ # and the coordinates of the stars that the line-segments interconnect. # # ACKNOWLEDGEMENTS # # The author wishes to thank the following people for assistance with this project: # # Daniel Castilo and Oscar Mendez Laesprella for encouragement, suggestions, bug # reports, beta testing and Spanish translation. Thanks, guys -- I couldn't have # done this without you. # # The owner and staff of The Java Room in Chelmsford, Massachusetts for the coffee, # wi-fi access and especially the live music. Best environment I've ever had for # developing code in! # # The members and officers of the Amateur Telescope Makers of Boston who encouraged # and educated me when I first discovered the wonder that is our planet's night sky. # =================================== IMPORTS =================================== from gi.repository import Gtk from gi.repository import Gdk import sys import os from math import sin, cos, tan, asin, acos, atan, pi, sqrt, atan2 import random import cairo from sugar3.activity import activity from sugar3.graphics.style import Color from sugar3.activity.widgets import ActivityToolbarButton from sugar3.activity.widgets import StopButton from sugar3.graphics.toolbutton import ToolButton from sugar3.activity.activity import get_bundle_path from sugar3.graphics.toolbarbox import ToolbarButton from sugar3.graphics.toolbarbox import ToolbarBox from sugar3.graphics.toolbutton import ToolButton import logging from gettext import gettext # Defensive method of gettext use: does not fail if the string is not translated. def _(s): istrsTest = {} for i in range (0,4): istrsTest[str(i)] = str(i) try: i = gettext(s) except: i = s return i # ------------------------------------------------------------------------------- # The bright star catalog is imported from stars1.py. import stars1 star_chart = stars1.data # The constellations figures have their own catalog. This catalog could potentially be # replaced by locale-specific figures, but that will break much of the code relating to # object locating and identifying, since the program wants to use the 88 IAU constellation # names (or at least their abbreviations). import constellations figures = constellations.data # ------------------------------------------------------------------------------- # # controls on second menubar ("Quiz"): labelq1 = Gtk.Label(_("Name")) cbq1 = Gtk.ComboBoxText() buttonq1 = ToolButton('ConstellationQuestion') buttonq1.set_tooltip(_("Tell me")) buttonq2 = ToolButton('ConstellationNext') buttonq2.set_tooltip(_("Another")) labelr1 = Gtk.Label(_(" constellations seen. ")) labelr3 = Gtk.Label(_(" correct on first try. ")) labelr4 = Gtk.Label(_(" correct on second try.")) name_from_abbrev = {} constellations = [] # # The program will bias the choice of constellation such that constellations with multiple # correct asnswers are chosen less frequently. The user gets five points for success on # the first try, three points for success on the second try, one point for success on the # third try and no points for needing 4 or more tries. If a constellation has more than # 50 points, we always skip it. If it has 26 to 50 points, we skip it 4 out of 5 times, # if it has 11 to 25 points, we skip it every other time. We do not skip constellations # which have 10 points or less. These two arrays are needed to manage this capability: # # We save the constellation scores whenever a correct answer is given so that they persist # between sessions. We also count sessions by updating an entry in the scores file. When # a session starts, we multiply the point score for each constellation by 0.8 as it is # read in. # colors = ['#FFFFFF', '#000000', '#CCCCCC'] score = {} seen = [] session_count = 1 quiz_count = 1 correct_first_count = 0 correct_second_count = 0 # ============================== START OF CODE ================================== # Because Python's trig is done in radians and I often need answers in degrees, # these two functions are provided to convert between radians and degrees. def dtor(a): return a * pi / 180.0 def rtod(r): return r * 180.0 / pi # ============================== ChartDisplay Object ============================ class ChartDisplay(Gtk.DrawingArea): def __init__(self, context): super(ChartDisplay, self).__init__() self.context = context self.canplot = False self.pangolayout = self.create_pango_layout("") self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | \ Gdk.ModifierType.BUTTON1_MASK | \ Gdk.ModifierType.BUTTON2_MASK) self.id = "" self.cname = "" self.points = 5 self.cnumber = 0 random.seed() def area_expose_cb(self, area, event): # Determine the area we can draw upon and adjust the chart accordingly. rect = self.get_allocation() self.screensize = (rect.width, rect.height) self.margin = 40 self.diameter = min(self.screensize[0], self.screensize[1]) - \ 2 * self.margin self.xoffset = (self.screensize[0] - self.diameter) / 2 - self.margin self.yoffset = (self.screensize[1] - self.diameter) / 2 - self.margin self.ctx = self.get_window().cairo_create() self.canplot = True self.plotchart(False) def callback(self, widget, data=None): global score global seen global quiz_count global correct_first_count global correct_second_count # Control callbacks are handled here. if (data == None): pass elif (data == "tell_me"): self.context.identifyobject.set_label(_("This constellation is named ") + \ self.cname) labelr1.set_label(str(quiz_count) + _(" constellations seen. ")) labelr3.set_label(str(correct_first_count) + _(" correct on first try. ")) labelr4.set_label(str(correct_second_count) + _(" correct on second try.")) cbq1.set_sensitive(False) buttonq1.set_sensitive(False) elif (data == "another"): self.context.identifyobject.set_label("") cbq1.set_sensitive(True) buttonq1.set_sensitive(True) quiz_count = quiz_count + 1 labelr1.set_label(str(quiz_count) + _(" constellations seen. ")) self.context.answer_status.set_text(_("")) labelr3.set_label(str(correct_first_count) + _(" correct on first try. ")) labelr4.set_label(str(correct_second_count) + _(" correct on second try.")) self.plotchart(True) elif (data == "select_name"): if (cbq1.get_active() >= 0): name = cbq1.get_active_text() if (name == self.cname): self.context.answer_status.set_text(_("That is correct.")) id = self.id score[id] = score[id] + self.points if (self.points == 5): correct_first_count = correct_first_count + 1 elif (self.points == 3): correct_second_count = correct_second_count + 1 self.context.update_config(self.context.datafile) self.points = 5 cbq1.set_sensitive(False) buttonq1.set_sensitive(False) else: self.context.answer_status.set_text(_("Sorry, that is not the correct name.")) self.points = self.points - 2 if (self.points < 0): self.points = 0 labelr1.set_label(str(quiz_count) + _(" constellations seen. ")) labelr3.set_label(str(correct_first_count) + _(" correct on first try. ")) labelr4.set_label(str(correct_second_count) + _(" correct on second try.")) elif data == 'reset_results': score = 0 seen = 0 quiz_count = 0 correct_first_count = 0 correct_second_count = 0 # Let's update the results now labelr1.set_label(str(quiz_count) + _(" constellations seen. ")) labelr3.set_label(str(correct_first_count) + _(" correct on first try. ")) labelr4.set_label(str(correct_second_count) + _(" correct on second try.")) self.context.write_file(self.context.datafile) else: pass return False # Convert equatorial coordinates to pixel position(x, y) in normalized form on a square # of self.diameter pixels. RA is in hours; dec is in degrees. def radectoxy(self, polar): ra0 = self.rac * 15.0 dec0 = self.decc ra = polar[0] dec = polar[1] ra = ra * 15 # If self.size is negative, this is a circumpolar constellation and is plotted on a circle # where 00h is down in the northern sky and up in the southern sky and the radius is # abs(size). Otherwise, the constellation is plotted in a square whose center is # (self.rac, self.decc) and whose size is self.size if (self.size < 0.0): if (dec0 > 0): r = (dec0 - dec) / -self.size * (self.diameter / 2.0) theta = 360.0 - ra y = 0.9 * r * cos(dtor(theta)) else: r = (dec0 - dec) / -self.size * (self.diameter / 2.0) theta = 180.0 - ra y = -0.9 * r * cos(dtor(theta)) x = 0.9 * r * sin(dtor(theta)) else: # Center the constellation and magnify to fit the chart. dec = dec0 - dec ra = ra0 - ra if (ra < -180.0): ra = ra + 360.0 if (ra > 180.0): ra = ra - 360.0 dec = dtor(dec) ra = dtor(ra) x = self.diameter / 2.0 * ra / pi y = self.diameter * dec / pi x = 0.9 * x * 360.0 / self.size y = 0.9 * y * 180.0 / self.size return (x, y) # ------------------------------------------------------------------------------- # # Methods for drawing the chart: def plotchart(self, newplot): if self.canplot: self.plot_field() self.plot_sky(newplot) return True def plot_field(self): # Erase prior plot if (not self.canplot): return self.cleararea() color_rgb = Color(colors[0]).get_rgba()[:-1] # we don't need alpha self.ctx.set_source_rgb(*color_rgb) self.ctx.rectangle(self.xoffset + self.margin - 2, self.yoffset + self.margin - 2, self.diameter + 4, self.diameter + 4) self.ctx.fill() # Plot sky square color_rgb = Color(colors[1]).get_rgba()[:-1] # we don't need alpha self.ctx.set_source_rgb(*color_rgb) self.ctx.rectangle(self.xoffset + self.margin - 2, self.yoffset + self.margin - 2, self.diameter + 4, self.diameter + 4) self.ctx.stroke() # label the cardinal points. self.ctx.move_to(self.xoffset + self.margin + self.diameter / 2 - 10, self.margin - 30) self.ctx.show_text(_("N")) self.ctx.move_to(self.xoffset + self.margin + self.diameter / 2 - 10, 2 * self.margin + self.diameter - 20) self.ctx.show_text(_("S")) self.ctx.move_to(self.xoffset + self.margin - 30, self.margin + self.diameter / 2 - 10) self.ctx.show_text(_("E")) self.ctx.move_to(self.xoffset + self.margin + self.diameter + 10, self.margin + self.diameter / 2 - 10) self.ctx.show_text(_("W")) color_rgb = Color(colors[1]).get_rgba()[:-1] # we don't need alpha self.ctx.set_source_rgb(*color_rgb) return True def plot_sky(self, choose): if (choose): self.cnumber = random.randrange(len(constellations)) self.id = self.pick_constellation() self.cname = name_from_abbrev[self.id] (self.rac, self.decc, self.size) = self.constellation_size(self.id) # self.context.identifyobject.set_label("rac=" + str(self.rac) +\ # " decc=" + str(self.decc) +\ # " size=" + str(self.size) ) self.plot_stars(self.id) self.plot_constellation(self.id) if (choose): self.fill_names_combobox() def plot_stars(self, id): # Plot the stars. # FIXME: Some stars are in more than one constellation. We need to make a special version # of the star catalog with duplicate entries for those cases. (E.g.: Auriga / Taurus and # Andromeda / Pegasus.) for name, (ra, dec, mag, cid) in star_chart.iteritems(): if (cid == id): # convert the ra and dec to pixel coordinates x and y (px, py) = self.radectoxy((ra, dec)) px = px + self.diameter / 2.0 py = py + self.diameter / 2.0 starsize = (4 + 2 * int(7.0 - mag))/3 px = px + self.margin - 2 + self.xoffset - starsize / 2 py = py + self.margin - 2 + self.yoffset - starsize / 2 if (mag <= 6.0): self.plot_star(px, py, starsize) def plot_constellation(self, id): # Plot the constellation figures. This is essentially the same process as for # plotting a star but we have to figure out the alt/az coordinates for both ends # of the line segment. for code, (name, lines) in figures.iteritems(): if (code == id): for i in range(len(lines)): (ra1, dec1, ra2, dec2) = lines[i] (px1, py1) = self.radectoxy((ra1, dec1)) px1 = px1 + self.diameter / 2.0 py1 = py1 + self.diameter / 2.0 px1 = px1 + self.margin - 2 + self.xoffset py1 = py1 + self.margin - 2 + self.yoffset (px2, py2) = self.radectoxy((ra2, dec2)) px2 = px2 + self.diameter / 2.0 py2 = py2 + self.diameter / 2.0 px2 = px2 + self.margin - 2 + self.xoffset py2 = py2 + self.margin - 2 + self.yoffset line_ctx = self.get_window().cairo_create() line_ctx.move_to(px1, py1) line_ctx.line_to(px2, py2) line_ctx.stroke() def plot_star(self, px, py, starsize): self.ctx = self.get_window().cairo_create() self.ctx.arc(px, py, starsize, 0, 360*64) self.ctx.fill() def pick_constellation(self): global seen # Using a random number between 0 and 87, select a constellation ID. id = -1 while (id < 0): id = constellations[self.cnumber] if (score[id] > 50): id = -1 # always skip if score > 50 elif (score[id] > 25) and (seen[self.cnumber] > 1): seen[self.cnumber] = seen[self.cnumber] - 1 id = -1 # skip 80% of the time if score between 26 and 50 elif (score[id] > 10) and (seen[self.cnumber] > 4): seen[self.cnumber] = seen[self.cnumber] - 1 id = -1 # skip 50% of the time if score between 11 and 25 else: # never skip pass seen[self.cnumber] = 5 return id def constellation_size(self, id): rac = 12.0 decc = 0.0 size = 360.0 ramin = 24.0 ramax = 0.0 decmin = 90.0 decmax = -90.0 # Since most constellations are plotted on a cylinder, we can determine their center # point and size simply by getting the coordinates of every star in the constellation # and determining the bounding rectangle. Then we return the center (ra, dec) and the # size (in ddegrees) of the square which would contain the constellation. In the case # where the constellation spans the ra = 00h meridian, we compensate for wrap-around # by adding 12h to every ra value and then subtracting 12h from the ra of the center # point. # # But some constellations are close to the celestial pole and a cylindrical projection # doesn't work. For these, we return the pole coordinate as the center, the minimum # (maximum for the south pole) declanation as the size but negative so that radectoxy() # knows that the projection is polar. For the purposes of this program, the circumpolar # constellations are defined as those whose decc is over 60 (or under -60) degrees. # These are: # Dra # Umi # Oct # Pav # Aps # Ara # Tra # Cha # Men # Hyi # Tuc for name, (ra, dec, mag, cid) in star_chart.iteritems(): if (cid == id): # FIXME: The assumption is that the stick figure contains no line whose end-point is # not already specified by a star's coordinates. It would be better to also enumerate # the stick-figure's endpoints when determining the constellation's boundaries. if (ra < ramin): ramin = ra elif (ra > ramax): ramax = ra if (dec < decmin): decmin = dec elif (dec > decmax): decmax = dec if not ( (id == "Aps") or (id == "Ara") or (id == "Cha") or (id == "Dra") or (id == "Hyi") or (id == "Men") or (id == "Oct") or (id == "Pav") or (id == "Tra") or (id == "Tuc") or (id == "Umi")): if (ramin <= 1.0) and (ramax >= 23.0): # This constellation spans the ra = 00h meridian. ramin = 24.0 ramax = 0.0 for name, (ra, dec, mag, cid) in star_chart.iteritems(): if (cid == id): ra = ra + 12.0 if (ra >= 24.0): ra = ra - 24.0 if (ra < ramin): ramin = ra elif (ra > ramax): ramax = ra rac = (ramin + ramax) / 2.0 - 12.0 if (rac < 0.0): rac = rac + 24.0 else: rac = (ramin + ramax) / 2.0 dra = ramax - ramin dra = dra * 15.0 decc = (decmin + decmax) / 2.0 ddec = decmax - decmin if (dra > ddec): size = dra else: size = ddec # Round off the size to the next higher multiple of 5 degrees. size = int(size / 5.0) + 1 size = size * 5.0 # Ensure that size is at least 30 degrees. if (size < 30.0): size = 30.0 return (rac, decc, size) # Handle the circumpolar constellations. elif ( (id == "Aps") or (id == "Ara") or (id == "Cha") or (id == "Hyi") or (id == "Men") or (id == "Pav") or (id == "Oct") or (id == "Tra") or (id == "Tuc")): # Round off the size to the next higher multiple of 5 degrees. size = 90.0 + decmax size = int(size / 5.0) + 1 size = size * 5.0 return (0, -90, -size) elif ( # (id == "Cam") or # (id == "Cep") or (id == "Dra") or (id == "Umi")): # Round off the size to the next higher multiple of 5 degrees. size = 90.0 - decmin size = int(size / 5.0) + 1 size = size * 5.0 return (0, 90, -size) def fill_names_combobox(self): global correct_first_count cbq1.remove_all() NUMBER_OF_CHOICES = 4 # Decide the NUMBER_OF_CHOICES if correct_first_count > 0: NUMBER_OF_CHOICES = int(10 * float(correct_first_count / \ len(constellations))) if NUMBER_OF_CHOICES < 4: NUMBER_OF_CHOICES = 4 # Create a list of NUMBER_OF_CHOICES names, initialized to "" names = [""] * NUMBER_OF_CHOICES numbers = [-1] * NUMBER_OF_CHOICES # Now set one of these names to self.cname. k = random.randrange(NUMBER_OF_CHOICES) names[k] = self.cname numbers[k] = self.cnumber # Choose len(names) - 1 additional constellation names (by random choice). # Add these names to the list, being sure not to overwrite any non-blank value # or use the same name twice. i = 0 for i in range(NUMBER_OF_CHOICES): r = random.randrange(len(constellations)) if not (r in numbers): id = constellations[r] cname = name_from_abbrev[id] for j in range(NUMBER_OF_CHOICES): if (names[j] == ""): names[j] = cname numbers[j] = r i = i + 1 break for i in range(NUMBER_OF_CHOICES): cbq1.append_text(names[i]) def cleararea(self): # Clear the drawing surface color_rgb = Color(colors[2]).get_rgba()[:-1] # we don't need alpha self.ctx = self.get_window().cairo_create() self.ctx.set_source_rgb(*color_rgb) self.ctx.rectangle(0, 0, self.screensize[0], self.screensize[1]) self.ctx.fill() # ========================= ConstellationsFlashCards Object ========================== class ConstellationsFlashCards(activity.Activity): def __init__(self, handle): global name_from_abbrev global constellations global score global seen activity.Activity.__init__(self, handle) self.datafile = os.path.join(activity.get_activity_root(),\ "data", "C_FC.cfg") self.chart = ChartDisplay(self) # Build the translation from constellation name to constellation ID (needed so we can # have a list of names to choose from). At the same time, make an array of constellation # IDs so the randomizer can pick one. for id in sorted(figures.keys()): (name, lines) = figures[id] name_from_abbrev[id] = name constellations.append(id) score[id] = 0 seen.append(5) # Create toolbox toolbar_box = ToolbarBox() # Activity Toolbar activity_button = ActivityToolbarButton(self) toolbar_box.toolbar.insert(activity_button, -1) # Quiz Toolbar self.quiz_toolbar = Gtk.Toolbar() label_container = Gtk.ToolItem() label_container.add(labelq1) label_container.show_all() self.quiz_toolbar.insert(label_container, -1) label_container = Gtk.ToolItem() label_container.add(cbq1) label_container.show_all() self.quiz_toolbar.insert(label_container, -1) self.quiz_toolbar.insert(buttonq1, -1) self.quiz_toolbar.insert(buttonq2, -1) buttonq1.show() buttonq2.show() label_container = Gtk.ToolItem() self.answer_status = Gtk.Label(_("")) label_container.add(self.answer_status) label_container.show_all() self.quiz_toolbar.insert(label_container, -1) self.answer_status.show() self.quiz_toolbar.show_all() quiz_toolbar_button = ToolbarButton( page=self.quiz_toolbar, icon_name='ConstellationNewGame') quiz_toolbar_button.show() toolbar_box.toolbar.insert(quiz_toolbar_button, -1) # Reset Results reset_button = ToolButton('system-restart') reset_button.set_tooltip(_('Reset Results')) reset_button.connect('clicked', self.chart.callback, 'reset_results') toolbar_box.toolbar.insert(reset_button, -1) reset_button.show() # Results hbox = Gtk.HBox() hbox.pack_start(labelr1, False, True, 0) hbox.pack_start(labelr3, False, True, 0) hbox.pack_start(labelr4, False, True, 0) hbox.show_all() label_container = Gtk.ToolItem() label_container.add(hbox) label_container.show_all() toolbar_box.toolbar.insert(label_container, -1) separator = Gtk.SeparatorToolItem() separator.props.draw = False separator.set_expand(True) toolbar_box.toolbar.insert(separator, -1) separator.show() # The stop button stop_button = StopButton(self) stop_button.props.accelerator = 'Q' toolbar_box.toolbar.insert(stop_button, -1) stop_button.show() self.set_toolbar_box(toolbar_box) toolbar_box.show_all() quiz_toolbar_button.set_expanded(True) # Create the GUI objects. scrolled = Gtk.ScrolledWindow() scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) eb = Gtk.EventBox() vbox = Gtk.VBox() self.identifyobject = Gtk.Label("") vbox.pack_start(self.identifyobject, False, True, 0) vbox.pack_start(self.chart, True, True, 0) self.identifyobject.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(*Color(colors[2]).get_rgba())) # Stack the GUI objects. scrolled.add_with_viewport(vbox) # Connect the event handlers self.chart.connect("draw", self.chart.area_expose_cb) buttonq1.connect("clicked", self.chart.callback, "tell_me") buttonq2.connect("clicked", self.chart.callback, "another") cbq1.connect("changed", self.chart.callback, "select_name") cbq1.set_sensitive(True) buttonq1.set_sensitive(True) # Set the canvas self.set_canvas(scrolled) # Show the GUI stack. self.chart.show() eb.show() scrolled.show() self.show_all() # If C_FC.cfg exists, get the previous scores. self.read_file(self.datafile) labelr1.set_label(str(quiz_count) + _(" constellations seen. ")) labelr3.set_label(str(correct_first_count) + _(" correct on first try. ")) labelr4.set_label(str(correct_second_count) + _(" correct on second try.")) # Establish initial state of controls and do a plot. self.chart.area_expose_cb(None, None) # make sure we have the surface ready self.chart.plotchart(True) def read_file(self, filename=""): global score global quiz_count global correct_first_count global correct_second_count global session_count # Read the values for the scores of all constellations. # We presently have no metadata to read. if (filename == ""): self.identifyobject.set_label("Read_file: no filename given.") return try: f = open(filename, "r") except: return try: for old_data in f: # Each line of interest consists of the three-character constellation ID, a colon and # an integer score. If the colon isn't present as character #4, see if the line is one # of the special-case directives like "seen:", "sessions:", "learned:" or "familiar:". if (old_data[3] == ':'): id = old_data[:3] points = int(old_data[4:]) score[id] = int(points * 0.8) elif (old_data[:5] == "seen:"): quiz_count = int(old_data[5:]) + 1 elif (old_data[:8] == "learned:"): correct_first_count = int(old_data[8:]) elif (old_data[:9] == "familiar:"): correct_second_count = int(old_data[9:]) elif (old_data[:9] == "sessions:"): session_count = int(old_data[9:]) + 1 else: pass except: pass f.close() def write_file(self, filename=""): # Write the values for the scores of all constellations. if (filename == ""): self.identifyobject.set_label("Write_file: no filename given.") return f = open(filename, "w") f.truncate(0) for i in range(len(constellations)): id = constellations[i] points = score[id] f.write(id + ":" + str(points) + '\n') f.write("sessions:" + str(session_count) + '\n') f.write("seen:" + str(quiz_count) + '\n') f.write("learned:" + str(correct_first_count) + '\n') f.write("familiar:" + str(correct_second_count) + '\n') f.close() # We presently have no metadata to write. def update_config(self, filename=""): # Modify the values for the scores of all constellations. if (filename == ""): self.identifyobject.set_label("Update_config: no filename given.") return data = [] try: f = open(filename, "r") except: f = open(filename, "w") else: try: for old_data in f: # Each line of interest consists of the three-character constellation ID, a colon and # an integer score. If the colon isn't present as character #4, see if the line is one # of the special-case directives like "sessions:", "learned:" or "familiar:". if (old_data[3] == ':'): pass elif (old_data[:8] == "learned:"): pass elif (old_data[:9] == "familiar:"): pass elif (old_data[:9] == "sessions:"): pass else: data.append(old_data) except: pass f.close() f = open(filename, "w") # Write all the non-conforming lines. for i in range(len(data)): f.write(data[i]) # Write the session count and results. f.write("sessions:" + str(session_count) + '\n') f.write("seen:" + str(quiz_count) + '\n') f.write("learned:" + str(correct_first_count) + '\n') f.write("familiar:" + str(correct_second_count) + '\n') # Now write the scores. for i in range(len(constellations)): id = constellations[i] points = score[id] f.write(id + ':' + str(points) + '\n') f.close()