Text-Based adventure in Kivy, Python - python

I'm very new to programming, just have an introductory seminar in university, and I'm supposed to program a little app in Python. For that, I want to use Kivy, but I got stuck.
I have a text file which should include the question, the possible answers and where it's supposed to go considering which answer the user chose:
0|"Will you rather do A or B?"|"A"|"B"|1|2
1|"Congrats, you chose A. Now go to B."|"Go to B"|"Go to B"|2|2
2|"That's B. Incredible. Want to discover C?"|"Yes."|"Stay here."|3|6
3|Wow, C is so cool, isn't it? There's also a D.|D? Crazy!|Boring. Go back.|4|0
4|Welcome to the depths of D. You are curious, aren't you?|Yep.|Nope.|5|0
5|Cool. There's nothing else here.|There must be.|Knew it.|4|0
6|Surprise! You should really discover C.|Alright.|Is there a D?|3|4
Now I want the game to go to the according line, replace the displayed text and go on. In theory, this is kind of working with my Code (I'm sorry if it's messed up, as I said, I'm new to this topic):
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
with open('try.txt') as t:
Lines = t.readlines()
first_line = Lines[0].strip()
x = first_line.split("|")
answer_b = int(x[5])
class MyGrid(GridLayout):
def __init__(self, **kwargs):
super(MyGrid, self).__init__(**kwargs)
self.cols = 1
self.inside = GridLayout()
self.inside.cols = 2
self.btna = Button(text=x[2])
self.btna.bind(on_press=self.a)
self.inside.add_widget(self.btna)
self.btnb = Button(text=x[3])
self.btnb.bind(on_press=self.b)
self.inside.add_widget(self.btnb)
self.main_text = Label(text=x[1])
self.add_widget(self.main_text)
self.add_widget(self.inside)
def a(self, instance):
answer_a = int(x[4])
next_line_a = Lines[answer_a].strip()
print(next_line_a)
print(answer_a)
x = next_line_a.split("|")
self.main_text.text = x[1]
self.btna.text = x[2]
self.btnb.text = x[3]
self.btna.bind(on_press=self.a)
def b(self, instance):
next_line_b = Lines[answer_b].strip()
print(next_line_b)
print(answer_b)
x = next_line_b.split("|")
self.main_text.text = x[1]
self.btna.text = x[2]
self.btnb.text = x[3]
class Game(App):
def build(self):
return MyGrid()
if __name__ == '__main__':
Game().run()
The problem is that it stays with the first line I defined and I don't really know how to go around that problem. I would imagine that I first define x with the first line, and after that x gets redefined with the according new line. But the next_line and x variable are both dependent on each other - I tried two different ways with answer a and b, but both don't really work. B will just continuously take the first_line-x, A tells me that x is referenced before assignment.
It would be great if someone could help me out of my confusion, because everything I tried just didn't work out...
Thanks!

I changed it so you pass items into the object that you create. It's challenging to get the inheritance correct.
I also added an initializer to the Games object. I think this works but to be honest I am not expert in the workings of Kivy and have gotten this pattern to work but I don't know for sure if it is best practice.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
with open('try.txt') as t:
Lines = t.readlines()
class MyGrid(GridLayout):
def __init__(self, Lines: list):
super(MyGrid, self).__init__()
self.first_line = Lines[0].strip()
self.xx = self.first_line.split("|")
self.answer_b = int(self.xx[5])
self.cols = 1
self.inside = GridLayout()
self.inside.cols = 2
self.btna = Button(text=self.xx[2])
self.btna.bind(on_press=self.a)
self.inside.add_widget(self.btna)
self.btnb = Button(text=self.xx[3])
self.btnb.bind(on_press=self.b)
self.inside.add_widget(self.btnb)
self.main_text = Label(text=self.xx[1])
self.add_widget(self.main_text)
self.add_widget(self.inside)
def a(self, instance):
answer_a = int(self.xx[4])
next_line_a = Lines[answer_a].strip()
print(next_line_a)
print(answer_a)
self.xx = next_line_a.split("|")
self.main_text.text = self.xx[1]
self.btna.text = self.xx[2]
self.btnb.text = self.xx[3]
self.btna.bind(on_press=self.a)
def b(self, instance):
next_line_b = Lines[self.answer_b].strip()
print(next_line_b)
print(self.answer_b)
self.xx = next_line_b.split("|")
self.main_text.text = self.xx[1]
self.btna.text = self.xx[2]
self.btnb.text = self.xx[3]
class Game(App):
def __init__(self, **kwargs):
self._arguments_to_pass_through = kwargs
super().__init__()
def build(self):
return MyGrid(**self._arguments_to_pass_through)
if __name__ == '__main__':
Game(Lines=Lines).run()

Related

Accessing Kivy Screenmanagaer inside a screen's __init__ method

I am trying to build an application that loads data from a JSON file and adds it to an MDList object. I would like the loaded items to take the user to a specific page when clicked. My implementation of the __init__ finction is shown bellow:
def __init__(self, **kw):
super().__init__(**kw)
json_data_object = JsonData("data.json")
# adds list item from JSON file to MDlist object
for i in json_data_object.data["lists"]:
loaded_object = ListItemWithoutCheckbox(text="[b]" + i["list_name"] + "[/b]")
self.ids["Container"].add_widget(loaded_object)
self.manager.add_widget(ToDoListPage(name=str(loaded_object.text)))
loaded_object.bind(on_release= lambda x : self.change_screen(loaded_object.text))
The first half of the for loop works as intended, adding the loaded objects to the MDList object. However the second half returns an AttributeError:
AttributeError: 'NoneType' object has no attribute 'add_widget'
I have a theory that this is due to the __init__ function running before the screen is added to the ScreenManager() object in the MainApp() class, shown below, but have no concrete proof of this nor any ideas for how to get around the issues.
class MainApp(MDApp):
def build(self):
# Setting theme to my favorite theme
self.theme_cls.theme_style = "Dark"
Builder.load_file("styling.kv")
sm = ScreenManager()
sm.add_widget(ToDoListView(name="ListView"))
return sm
I will keep trying to work on the issue but I am struggling to come up with new ideas, so any help is greatly appreciated.
If I manage to crack this I will post my solution to this issue!
Thanks for any help :)
EDIT:
I have added a method to attempt to add the on_release functionality after the __init__ method has run. On printing self.parent I still receive a None value. I have also attempted to use the Clock module of kivy as suggested in the comments by John Anderson but I am still receiving the same AttributeError shown earlier in this question. My edited code now looks like this:
Class initialisation:
def __init__(self, **kw):
super().__init__(**kw)
json_data_object = JsonData("data.json")
self.loaded_items = []
# adds list item from JSON file to MDlist object
for i in json_data_object.data["lists"]:
loaded_object = ListItemWithoutCheckbox(text="[b]" + i["list_name"] + "[/b]")
self.ids["Container"].add_widget(loaded_object)
self.loaded_items.append(loaded_object)
def load_tasks(self, loaded_objects):
for i in loaded_objects:
# To test if i can access the screen manager
print(self.parent)
self.manager.add_widget(ToDoListPage(name=str(i.text)))
object.bind(on_release= lambda x : self.change_screen(i.text))
Main app class:
class MainApp(MDApp):
def build(self):
# Setting theme to my favorite theme
self.theme_cls.theme_style = "Dark"
Builder.load_file("styling.kv")
list_view_screen = ToDoListView(name = "ListView")
list_view_screen.load_tasks(list_view_screen.loaded_items)
sm = ScreenManager()
sm.add_widget(list_view_screen)
return sm
Thank you so much for even checking out this question!
any help is greatly appreciated :)
happy to say i finally found a solution to the issue. By abstracting the functionality of the __init__ method into another method and making a partial call in kivy's Clock object, the function could utilise the screen manager object.
def __init__(self, sm,**kw):
super().__init__(**kw)
Clock.schedule_once(partial(self.load_tasks, sm))
def load_tasks(self, sm, *largs):
json_data_object = JsonData("data.json")
self.loaded_items = []
for i in json_data_object.data["lists"]:
self.add_loaded_item_to_list(i)
for i in self.loaded_items:
self.bind_on_release_to_loaded_item(sm, i)
def bind_on_release_to_loaded_item(self, sm, loaded_item):
self.manager.add_widget(ToDoListPage(name = loaded_item.text))
loaded_item.bind(on_release= lambda x: self.change_screen(loaded_item.text))
def add_loaded_item_to_list(self, loaded_item):
loaded_object = ListItemWithoutCheckbox(text="[b]" + loaded_item["list_name"] + "[/b]")
self.ids["Container"].add_widget(loaded_object)
self.loaded_items.append(loaded_object)
The MainApp class reverted to its original:
class MainApp(MDApp):
def build(self):
# Setting theme to my favorite theme
self.theme_cls.theme_style = "Dark"
Builder.load_file("styling.kv")
sm = ScreenManager()
sm.add_widget(ToDoListView(sm,name = "ListView"))
return sm
Thanks again to John Anderson for helping me get to this solution :)

Python UNO on LibreOffice Calc, rehoming a cursor

LibreOffice 5.3, python 3.53, VOID Linux
This is more of an uno question than a python question. The code below does a simple update of 3 cells. 3 buttons configured on the sheet calling dowriteonce() dowritetwice() and dowritethrice(), and they all update and work like you might expect writing numbers and text to selected cells.
Where the problem comes in, is that when a cell is edited in the UI by a user, any subsequent update of that cell by means of executing the function is blocked. So simply clicking cell C4 in the calc UI, prevents the writethrice() function from updating cell C4. If I delete the content and click another cell in the UI, say C5, then everything works normally again and C4 updates when the button is clicked.
What I would like to do is relocate the UI edit-cursor to an unused cell prior to execution in order to prevent this. User copy-paste is going to leave the active cursor in unpredictable places and that will bork calculations if I can't isolate the cursor.
So the question is, how do I move the UI edit cursor to a named cell via the UNO API, with Python? Or if it is easier, just deactivate it temporarily.
Python:
import socket
import sys
import re
import uno
import unohelper
class ODSCursor(unohelper.Base):
# predeclare class properties
ctx=None
desktop=None
model=None
activesheet=None
counter=0
scooby="Scooby"
# import namespaces
def __init__(self):
import socket
import uno
import unohelper
import sys
import re
# initialize uno handle only once and get the first sheet
#classmethod
def sheet1(cls,*args):
if cls.activesheet is not None:
return (cls.activesheet)
cls.ctx = uno.getComponentContext()
cls.desktop = cls.ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", cls.ctx)
cls.model = cls.desktop.getCurrentComponent()
# cls.activesheet = cls.model.Sheets.getByName("Sheet1")
cls.activesheet = cls.model.Sheets.getByIndex(0)
return (cls.activesheet)
#classmethod
def writeonce(self,*args):
self.counter += 1
cell_b1 = self.activesheet.getCellRangeByName("B1")
cell_b1.String = self.counter
#classmethod
def writetwice(self,*args):
self.counter += 1
cell_b2 = self.activesheet.getCellRangeByName("B2")
cell_b2.String = self.counter
#classmethod
def writescooby(self,*args):
cell_c4 = self.activesheet.getCellRangeByName("C4")
cell_c4.String = self.scooby
### BUTTON BOUND FUNCTIONS ###
def dowriteonce(*args):
Odc = ODSCursor() # create the object
Odc.sheet1()
Odc.writeonce()
def dowritetwice(*args):
Odc = ODSCursor() # create the object
Odc.sheet1()
Odc.writetwice()
def dowritethrice(*args):
Odc = ODSCursor() # create the object
Odc.sheet1()
Odc.writescooby()
In the following code, cells are deselected before changing the values, then selected again. This way, cells can be modified even when left in edit mode by the user.
There also seems to be some confusion about Python class methods and variables, so I changed those parts as well.
import uno
import unohelper
SCOOBY = "Scooby"
class ODSCursor(unohelper.Base):
def __init__(self):
self.ctx = None
self.desktop = None
self.document = None
self.controller = None
self.sheet = None
self.counter = 0
def sheet1(self):
"""Initialize uno handle only once and get the first sheet."""
if self.sheet is not None:
return self.sheet
self.ctx = uno.getComponentContext()
self.desktop = self.ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.frame.Desktop", self.ctx)
self.document = self.desktop.getCurrentComponent()
self.controller = self.document.getCurrentController()
self.sheet = self.controller.getActiveSheet()
return self.sheet
def writeonce(self):
self.writeval("B1", self.inc())
def writetwice(self):
self.writeval("B2", self.inc())
def writescooby(self):
self.writeval("C4", SCOOBY)
def writeval(self, address, value):
self.deselect()
cell = self.sheet.getCellRangeByName(address)
cell.String = value
self.controller.select(cell)
def deselect(self):
"""Select cell A1, then select nothing."""
cell_a1 = self.sheet.getCellByPosition(0, 0)
self.controller.select(cell_a1)
emptyRanges = self.document.createInstance(
"com.sun.star.sheet.SheetCellRanges")
self.controller.select(emptyRanges)
def inc(self):
"""Increment the counter and return the value."""
self.counter += 1
return self.counter
odsc = ODSCursor()
### BUTTON BOUND FUNCTIONS ###
def dowriteonce(dummy_oEvent):
odsc.sheet1()
odsc.writeonce()
def dowritetwice(dummy_oEvent):
odsc.sheet1()
odsc.writetwice()
def dowritethrice(dummy_oEvent):
odsc.sheet1()
odsc.writescooby()

Data from child window

In the imported code, the variable 'values' is set correctly with the date selected by the user.
The def selection is called at the exit of the calendar.
I'm stuck. I don't know how to catch it and use it in my main code.
Thanks a lot.
# MAIN CODE (simplified)
from tkinter import *
import calendarWidget
def manageCalendarWindow():
root4 = Tk()
data = {}
app = calendarWidget.Calendar(root4, data)
root4.mainloop()
manageCalendarWindow()
#how to get the date?
-.-.-.-.-.-.-.
# CALENDAR WIDGET (simplified)
class Calendar:
def setup(self, y, m)
(...)
for w, week in enumerate(self.cal.monthdayscalendar(y, m), 2):
for d, day in enumerate(week):
if day:
b = tk.Button(self.parent, width=1, text=day, relief = 'flat',\
command=lambda day=day:self.selection(day, calendar.day_name[(day-1) % 7]))
self.wid.append(b)
b.grid(row=w, column=d)
def selection(self, day, name):
self.day_selected = day
self.month_selected = self.month
self.year_selected = self.year
self.day_name = name
#data
self.values['day_selected'] = day
self.values['month_selected'] = self.month
self.values['year_selected'] = self.year
self.values['day_name'] = name
self.values['month_name'] = calendar.month_name[self.month_selected]
self.setup(self.year, self.month)
print(self.values) # <--- here the value is correct
self.parent.destroy()
-.-.-.-.-.-.-.-
THIS WORKS:
def manageCalendarWindow():
root4 = Tk()
data = {}
app = calendarWidget.Calendar(root4, data)
root4.mainloop()
return app
app=manageCalendarWindow()
print(app.year_selected,app.month_selected,app.day_selected)
THIS NOT:
class enterWindows():
def B_CalendarWindow(self):`
app=self.manageCalendarWindow()
print("year: ",app.year_selected)
print("and... this will never be printed!")
def manageCalendarWindow(self):
root4 = Tk()
data = {}
app = calendarWidget.Calendar(root4, data)
root4.mainloop()
return app
Everything local to the function manageCalendarWindow() is garbage collected when the function exits. This includes app (the class instance). You would have to return it to keep it alive. Note also that there is no self.month in the code you posted but I assume that comes from cutting the amount of code back for this post.
def manageCalendarWindow():
root4 = Tk()
data = {}
app = calendarWidget.Calendar(root4, data)
root4.mainloop()
return app
## or
## return app.day_name
app=manageCalendarWindow()
print(app.day_name)
## or
##day_name=manageCalendarWindow()
##print(day_name)
A simple proof-of concept program that gets a variable from a class instantiated within the class.
class SubClass():
def __init__(self):
self.variable="SubClass variable"
class MainClass():
def __init__(self):
app=self.instantiate_class()
print(app.variable) ## prints the string
def instantiate_class(self):
app=SubClass()
print("app =", app) ## not empty
return app
MainClass()
As mentioned in my comments, there can be only one mainloop in the code. The others will be simply ignored. So it's not possible to use it to wait for a response from a child window.
The solution I used is
app = myCalendar.Calendar(personal_path, root4, gui_language)
root4.wait_window(app.parent)
return app
The code opens the window and waits the result using wait_window().
Thanks all.
Ciao.

kivy button text="" logic error

I am new to python and kivy, I am trying to learn python bymaking a small minesweeper game, however, I feel the logic in below code is correct, but somehow seems not to work: complete file is as follows:
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
import random
class spot(Button):
'''
classdocs
'''
def __init__(self, **kwargs):
'''
Constructor
'''
super(spot,self).__init__(**kwargs)
self.ismine=False
self.text="X"
if 'id' in kwargs:
self.id=kwargs['id']
#else:
# self.text="X"
class minesholder(GridLayout):
def __init__(self,**kwargs):
super(minesholder,self).__init__(**kwargs)
class game(BoxLayout):
spots={}
mines=[]
def __init__(self,**kwargs):
super(game,self).__init__(**kwargs)
self.m=minesholder(rows=5, cols=5)
self.add_widget(self.m)
self.attachtogrid()
def attachtogrid(self):
self.m.clear_widgets()
self.spots.clear()
for r in range(0,5):
for c in range(0,5):
idd=str(r)+","+str(c)
self.spots[idd]=idd
s = spot(id=idd)
self.m.add_widget(s)
s.bind(on_press=self.spottouched)
print(idd)
self.createmines()
def createmines(self):
self.mines.clear()
count=0
while (count <= 10):
c=str(random.randint(0,4))+','+str(random.randint(0,4))
print(c)
if self.mines.count(c)==0:
self.mines.append(c)
count+=1
def spottouched(self,spotted):
#if self.mines.count(str(spotted.id))==1:
# spotted.text="BOMB"
#else: spotted.text=""
for k in self.mines:
if k==spotted.id: spotted.text="BOMB"
else:
spotted.text=""
The issue is the last 4 lines, when I remove the "spotted.text=""", the code is working perfect, but when I keep the text="" the code is not working any more, despite of having 11 bombs only 1 is getting detected, with out the text="", all the bombs are getting detected correctly (text="BOMB" works).
Each time spottouched() is called, you loop through each mine and set the text accordingly. But let's say you have two bombs - let's call the bombs ['bomb-a', 'bomb-b'].
Now you touch the button with the id 'bomb-a'. spottouched() loops through the mines. The first mine in the list is 'bomb-a' - so it sets the text to "BOMB". Then it loops - the second mine in the list is 'bomb-b', so the text is set back to "". So the only mine which will show the "BOMB" text is the very last mine in the list.
Try something like this instead:
def spottouched(self, spotted):
spotted.text = "BOMB" if spotted.id in self.mines else ""

NSMenuItem + Python + Dynamic Variables

I'm trying to get a dynamic label for a menu item in this. I've written the entire app in Python but honestly with how much NSMenuItem looks, I might as well rewrite it in Objc...
import objc
from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper
class MyApp(NSApplication):
def finishLaunching(self):
# Make statusbar item
statusbar = NSStatusBar.systemStatusBar()
self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength)
self.icon = NSImage.alloc().initByReferencingFile_('icon.png')
self.icon.setScalesWhenResized_(True)
self.icon.setSize_((20, 20))
self.statusitem.setImage_(self.icon)
#make the menu
self.menubarMenu = NSMenu.alloc().init()
self.menuItem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Click Me', 'clicked:', '')
self.menubarMenu.addItem_(self.menuItem)
self.quit = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Quit', 'terminate:', '')
self.menubarMenu.addItem_(self.quit)
#add menu to statusitem
self.statusitem.setMenu_(self.menubarMenu)
self.statusitem.setToolTip_('My App')
def clicked_(self, notification):
NSLog('clicked!')
if __name__ == "__main__":
app = MyApp.sharedApplication()
AppHelper.runEventLoop()
Did you try setTitle_?
def clicked_(self, notification):
self.menuItem.setTitle_("Clicked!")
or with a timer:
def finishLaunching(self):
# ...
self.timer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_(NSDate.date(), 1.0, self, 'tick:', None, True)
NSRunLoop.currentRunLoop().addTimer_forMode_(self.timer, NSDefaultRunLoopMode)
self.timer.fire()
def tick_(self, arg):
self.menuItem.setTitle_("Tick %d!" % int(time.time()))
For live updates you probably need JGMenuWindow (SO: How to update NSMenu while it's open?)

Categories