Building Log Hacker
Play it first → You have broken into a server. One of the words on the screen is its password, and you have four tries. Click a word to guess it. Every guess goes in the log, with how many of its letters are the right letter in the right place, and the password has to match every line of the log. Get it, and the next server's password is longer. There is no clock.
This is how it was built, a step at a time. Every numbered step below is a whole program: copy it into the playground, press Run, and it works. Each step is the one before it with something added, so you can build the game the way it was built. Then break something on purpose and see what changes. That teaches you more than reading does.
There is nothing clever in it. No list comprehensions, no tricks, and every kaypy function it uses is on the API page. There are no pictures at all, only words, and four sounds from the playground.
It is the game for
Lesson 9 — Files, and it comes after
Goblin Hit. It uses the same pieces:
functions, global, wait(), onClick with
isHovering(), and a scene. If any of those are new, build Goblin
Hit first. After this one, try Paddle Ball.
The one big idea: three files
A real hacker would find the files already on the server. This game has no server, so it makes its own, and then reads them back exactly as it would read any file:
words.txt— the words the password could be, one on each line.password.txt— the one it is.log.txt— every guess so far, and how close it was.
Every file is opened the same way:
with open("words.txt", "w") as words_file:
words_file.write("crash\n")
open() opens the file, and as words_file gives it a
name for the indented lines underneath. When the indenting ends, the file is
closed by itself. The second thing in open() says what you're
going to do with it: "w" to write, "r" to read,
"a" to add to the end.
Run it on your own computer and the three files appear in the folder your program runs in, where you can open them and look. In the playground they are just as real, but they live inside the page. They last from one Run to the next, and they are gone when you reload.
Step 1 — a file of words
Write eight words into a file, read them back, and put them on the screen.
from kaypy import *
kaypy(width=800, height=600, background=[8, 14, 10])
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
WORDS = ["crash", "crack", "track", "trace", "trick", "brick", "block",
"clock"]
# Write the file. with ... as words_file: opens it, and calls it
# words_file for the indented lines underneath. When they end, the file
# is closed by itself. "w" means write: it makes words.txt, or empties it
# if it is already there.
with open("words.txt", "w") as words_file:
for word in WORDS:
words_file.write(word + "\n")
# Read it back. "r" means read. A for loop over a file goes round once for
# each line, and line is the line, "\n" and all. strip() takes it off.
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
# The words, down the left-hand side.
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN)])
y = y + 32
"w"means write. It makeswords.txt, or empties it first if it's already there.write()puts exactly what you give it into the file, and nothing else. It doesn't start a new line for you."\n"is the end of a line, soword + "\n"is the word and then a new line."r"means read.for line in words_filegoes round once for each line in the file, top to bottom, and each timelineis the next one.linestill has its"\n"on the end. The first time round it is"crash\n", not"crash".strip()takes it off, along with any spaces at either end.
Break it. Take the
+ "\n" out of the write() line. There is no error,
but there is only one word now:
crashcracktracktracetrickbrickblockclock.
The words went into the file one after another with nothing between them, so the file is one line long, and reading it back, the loop finds one line.
Break it. Put the "\n"
back, and add one more line after the first with, right at the
left edge, not indented:
words_file.write("slash\n")
It stops straight away with
ValueError: I/O operation on closed file.
The file was closed when the indenting ended. words_file still
names it, but you can't write to a closed file. Everything you do with a file
goes inside its with.
Step 2 — the password, and guessing
One of the words is the password, and it goes in a file of its own. Click a word to guess it.
from kaypy import *
kaypy(width=800, height=600, background=[8, 14, 10])
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
WORDS = ["crash", "crack", "track", "trace", "trick", "brick", "block",
"clock"]
# Write the file. with ... as words_file: opens it, and calls it
# words_file for the indented lines underneath. When they end, the file
# is closed by itself. "w" means write: it makes words.txt, or empties it
# if it is already there.
with open("words.txt", "w") as words_file:
for word in WORDS:
words_file.write(word + "\n")
# Read it back. "r" means read. A for loop over a file goes round once for
# each line, and line is the line, "\n" and all. strip() takes it off.
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
# One of them is the password, and it goes in a file of its own.
with open("password.txt", "w") as password_file:
password_file.write(choose(words) + "\n")
def read_password():
# read() is the whole file, as one string.
with open("password.txt", "r") as password_file:
password = password_file.read().strip()
return password
# The words, down the left-hand side. Each one is something to click:
# area() lets it know when the mouse is over it, and the dictionary gives
# it a value of its own, the word.
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN), area(),
{"word": word}, "word"])
y = y + 32
message = add([text("", size=36), pos(560, 450), anchor("center"),
color(GREEN)])
# onClick runs on every click, wherever it is, and a tap on a phone counts
# as a click. isHovering() asks whether the mouse is over a word.
@onClick
def click():
for row in get("word"):
if row.isHovering():
guess(row)
return
def guess(row):
if row.word == read_password():
message.text = "ACCESS GRANTED"
else:
message.text = "WRONG"
row.color = DIM
choose(words)picks one of the words at random, and it is written intopassword.txt.read()hands back the whole file as one string, soread_password()gives"brick\n", andstrip()makes it"brick". The program never keeps the password in a variable for long: it reads it from the file every time you guess.- Each word has
area(), so it knows when the mouse is over it, and{"word": word}, so it remembers its word asrow.word. @onClickrunsclickon every click, wherever it is, and a tap on a phone counts. It goes through the words and guesses the one the mouse is over. A wrong one goes dim.
Break it. In
read_password, take .strip() off. Now click every
word, one after another. Every one is WRONG, including the password.
Without strip() the password is "brick\n", and
"brick" is not the same string. A "\n" doesn't show
on the screen, so nothing looks wrong. This is the most common mistake there
is with files, and it never makes an error message.
Step 3 — how close, and the log
Guessing at random isn't hacking. Now every guess goes in the log, with how many of its letters are the right letter in the right place.
from kaypy import *
kaypy(width=800, height=600, background=[8, 14, 10])
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
WORDS = ["crash", "crack", "track", "trace", "trick", "brick", "block",
"clock"]
# Write the file. with ... as words_file: opens it, and calls it
# words_file for the indented lines underneath. When they end, the file
# is closed by itself. "w" means write: it makes words.txt, or empties it
# if it is already there.
with open("words.txt", "w") as words_file:
for word in WORDS:
words_file.write(word + "\n")
# Read it back. "r" means read. A for loop over a file goes round once for
# each line, and line is the line, "\n" and all. strip() takes it off.
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
# One of them is the password, and it goes in a file of its own. And a
# log, with nothing in it yet.
with open("password.txt", "w") as password_file:
password_file.write(choose(words) + "\n")
with open("log.txt", "w") as log_file:
log_file.write("")
def read_password():
# read() is the whole file, as one string.
with open("password.txt", "r") as password_file:
password = password_file.read().strip()
return password
def read_log():
# Each line of the log is a guess and how many letters it got right,
# with a comma between: crash,2
guesses = []
with open("log.txt", "r") as log_file:
for line in log_file:
guesses.append(line.strip())
return guesses
# How many letters of the guess are the right letter in the right place.
# crash and crack share c, r, a in the same places, so that is 3.
def letters_right(guess, password):
right = 0
for i in range(len(password)):
if guess[i] == password[i]:
right = right + 1
return right
# The words, down the left-hand side. Each one is something to click:
# area() lets it know when the mouse is over it, and the dictionary gives
# it a value of its own, the word.
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
add([text("log.txt", size=20), pos(330, 100), color(DIM)])
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN), area(),
{"word": word}, "word"])
y = y + 32
message = add([text("", size=36), pos(560, 450), anchor("center"),
color(GREEN)])
# onClick runs on every click, wherever it is, and a tap on a phone counts
# as a click. isHovering() asks whether the mouse is over a word.
@onClick
def click():
for row in get("word"):
if row.isHovering():
guess(row)
return
# Read log.txt and put every line of it on the screen. The old lines are
# taken away first, so each guess is shown once.
def show_log():
destroyAll("entry")
y = 130
for guess in read_log():
# "crash,2" becomes ["crash", "2"]: split(",") cuts it up at the
# comma.
parts = guess.split(",")
add([text("> " + parts[0], size=24), pos(340, y), color(GREEN),
"entry"])
add([text(parts[1] + "/5 right", size=24), pos(500, y), color(DIM),
"entry"])
y = y + 32
def guess(row):
password = read_password()
right = letters_right(row.word, password)
# The guess goes on the end of the log. "a" means add: it keeps what
# is there and writes after it, where "w" would empty it.
with open("log.txt", "a") as log_file:
log_file.write(row.word + "," + str(right) + "\n")
show_log()
if row.word == password:
message.text = "ACCESS GRANTED"
else:
row.color = DIM
letters_rightgoes along the password one letter at a time.range(len(password))is 0, 1, 2, 3, 4 for a five-letter password, andguess[i]is the letter at that place. If the guess has the same letter in the same place, that's one more right.crashandcrackshare c, r and a in the same places, so that's 3."a"means add. It keeps what is in the file and writes after it. Every guess goes on the end oflog.txtas a line likecrash,3.show_log()reads the whole log back and puts it on the screen.split(",")cuts"crash,3"at the comma into["crash", "3"]. It takes the old lines away first, withdestroyAll("entry"), so each guess is shown once.- At the start, the log is opened with
"w"and nothing written, which empties it, so it doesn't still hold the guesses from last time you pressed Run.
Now you can work it out. Say you guess clock and get
2/5. The password has exactly two letters where
clock has them. track shares only the
ck at the end with clock, so it could be the
password. crash shares only the c at the start, so
it can't. Every line of the log rules words out.
Break it. In guess,
change open("log.txt", "a") to open("log.txt", "w").
There is no error, but the log only ever shows one line: the last guess.
"w" empties the file every time, so every guess wipes out the one
before.
Step 4 — four tries
Now it's a game. You get four tries, and then you're locked out. Writing the files goes into a function, and the whole game goes into a scene, so it can start again with new files.
from kaypy import *
kaypy(width=800, height=600, background=[8, 14, 10])
WORDS = ["admin", "login", "guest", "pixel", "cyber", "virus", "token",
"cache", "patch", "debug", "shell", "stack", "crash", "crack",
"track", "trace", "trick", "brick", "block", "clock", "click",
"flash", "stash", "slash"]
TRIES = 4 # how many guesses you get
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
BRIGHT = rgb(200, 255, 210)
def write_files(how_many):
# Pick different words until there are enough: a word that is already
# picked is skipped.
words = []
while len(words) < how_many:
word = choose(WORDS)
if word not in words:
words.append(word)
with open("words.txt", "w") as words_file:
for word in words:
words_file.write(word + "\n")
# One of them is the password.
with open("password.txt", "w") as password_file:
password_file.write(choose(words) + "\n")
# A new log, with nothing in it yet. "w" empties it.
with open("log.txt", "w") as log_file:
log_file.write("")
def read_words():
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
return words
def read_password():
# read() is the whole file, as one string.
with open("password.txt", "r") as password_file:
password = password_file.read().strip()
return password
def read_log():
# Each line of the log is a guess and how many letters it got right,
# with a comma between: crash,2
guesses = []
with open("log.txt", "r") as log_file:
for line in log_file:
guesses.append(line.strip())
return guesses
# How many letters of the guess are the right letter in the right place.
# crash and crack share c, r, a in the same places, so that is 3.
def letters_right(guess, password):
right = 0
for i in range(len(password)):
if guess[i] == password[i]:
right = right + 1
return right
tries_left = TRIES
playing = True
over = False
@scene("server")
def server():
global tries_left, playing, over
# A new game: new files, written and then read.
write_files(8)
words = read_words()
tries_left = TRIES
playing = True
over = False
# ------------------------------------------------------- the screen
add([text("SERVER", size=28), pos(20, 20), color(GREEN)])
tries_label = add([text("TRIES LEFT " + str(TRIES), size=24),
pos(780, 24), anchor("topright"), color(GREEN)])
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
add([text("log.txt", size=20), pos(330, 100), color(DIM)])
# The words, read from words.txt. Each one is something to click:
# area() lets it know when the mouse is over it, and the dictionary
# gives it two values of its own -- the word, and whether it has been
# tried yet.
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN), area(),
{"word": word, "tried": False}, "word"])
y = y + 32
message = add([text("", size=36), pos(560, 450), anchor("center"),
color(GREEN), z(10)])
under = add([text("", size=22), pos(560, 495), anchor("center"),
color(GREEN), z(10)])
# ------------------------------------------------------------ the log
#
# Read log.txt and put every line of it on the screen. The old lines
# are taken away first, so each guess is shown once.
def show_log():
destroyAll("entry")
y = 130
for guess in read_log():
# "crash,2" becomes ["crash", "2"]: split(",") cuts it up at
# the comma.
parts = guess.split(",")
add([text("> " + parts[0], size=24), pos(340, y), color(GREEN),
"entry"])
add([text(parts[1] + "/" + "5 right", size=24),
pos(500, y), color(DIM), "entry"])
y = y + 32
# ------------------------------------------------------------ guessing
# A word lights up while the mouse is over it, unless it has been tried.
@onUpdate("word")
def light_up(row):
if row.tried:
row.color = DIM
elif row.isHovering() and playing:
row.color = BRIGHT
else:
row.color = GREEN
# onClick runs on every click, wherever it is, and a tap on a phone
# counts as a click. isHovering() asks whether the mouse is over a word,
# which needs area() on it.
@onClick
def click():
if not playing:
return
for row in get("word"):
if row.isHovering() and not row.tried:
guess(row)
return
def guess(row):
global tries_left
row.tried = True
password = read_password()
right = letters_right(row.word, password)
# The guess goes on the end of the log. "a" means add: it keeps
# what is there and writes after it, where "w" would empty it.
with open("log.txt", "a") as log_file:
log_file.write(row.word + "," + str(right) + "\n")
show_log()
if row.word == password:
granted(password)
return
tries_left = tries_left - 1
tries_label.text = "TRIES LEFT " + str(tries_left)
shake(4)
if tries_left == 0:
locked_out(password)
# --------------------------------------------------- in, or locked out
def granted(password):
global playing
playing = False
message.text = "ACCESS GRANTED"
under.text = "the password was " + password
wait(1, ready_again)
def locked_out(password):
global playing
playing = False
shake(12)
message.text = "LOCKED OUT"
message.color = RED
under.text = "it was " + password
# A second before you can start again, so a click meant for a word
# does not skip past what happened.
wait(1, ready_again)
def ready_again():
global over
over = True
add([text("press SPACE or click to start again", size=22),
pos(560, 540), anchor("center"), color(GREEN)])
def again():
if over:
go("server")
onKeyPress("space", again)
onClick(again)
go("server")
write_files(8)writes all three files. Itswhileloop picks words until there are eight different ones: a word already picked is skipped. Aforloop would go round exactly eight times and could pick the same word twice.- Everything is inside
@scene("server"), sogo("server")starts it again from the top: new words, a new password, an empty log, and four tries. - Each word has two values of its own now,
{"word": word, "tried": False}. Once you've guessed a word it istried, so it goes dim and clicking it again does nothing. A word you haven't tried lights up when the mouse is over it. tries_leftgoes down by one for every wrong guess. At 0 it'slocked_out: the game says what the password was, and waits a second before you can start again, so a click meant for a word doesn't start a new game before you've seen it.global:tries_left,playingandoverare made at the top of the program and changed inside functions, so every function that changes one saysglobalfirst.
Break it. In
write_files, take out the last two lines, the
with that empties the log. Play a game to the end and start
again. The new game's log already has the last game's guesses in it, and how
close they were was worked out against the last game's password, so they
tell you nothing about this one.
A file stays until something changes it. That's what files are for, and it's why the log has to be emptied on purpose.
Step 5 — harder servers
Get in, and there is another server, with more words and a longer password.
from kaypy import *
kaypy(width=800, height=600, background=[8, 14, 10])
WORDS = ["admin", "login", "guest", "pixel", "cyber", "virus", "token",
"cache", "patch", "debug", "shell", "stack", "crash", "crack",
"track", "trace", "trick", "brick", "block", "clock", "click",
"flash", "stash", "slash",
"server", "binary", "cookie", "kernel", "packet", "router",
"script", "secret", "socket", "pocket", "rocket", "locket",
"system", "matrix", "shadow", "access", "backup", "buffer",
"hacker", "logger", "loader", "locker", "master", "poster",
"account", "browser", "network", "malware", "monitor", "program",
"console", "hacking", "decrypt", "encrypt", "virtual", "digital",
"gateway", "storage", "process", "compile", "install", "payload",
"cracker", "tracker", "packets", "sockets", "rockets", "pockets"]
TRIES = 4 # how many guesses you get on each server
WORDS_START = 8 # how many words the first server has
WORDS_MORE = 1 # ...one more on each server after it
WORDS_MOST = 14 # ...and never more than this, or they run off the screen
LENGTH_START = 5 # how long the first server's password is
LENGTH_MOST = 7 # every two servers it is one longer, up to this
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
BRIGHT = rgb(200, 255, 210)
def write_files(length, how_many):
# Every word in WORDS that is the right length.
fits = []
for word in WORDS:
if len(word) == length:
fits.append(word)
# The while loop below picks different words until it has enough. Ask
# it for more than there are and it could never finish: it would go
# round for ever, and the game would freeze. So never ask for more.
if how_many > len(fits):
how_many = len(fits)
words = []
while len(words) < how_many:
word = choose(fits)
if word not in words:
words.append(word)
with open("words.txt", "w") as words_file:
for word in words:
words_file.write(word + "\n")
# One of them is the password.
with open("password.txt", "w") as password_file:
password_file.write(choose(words) + "\n")
# A new log, with nothing in it yet. "w" empties it.
with open("log.txt", "w") as log_file:
log_file.write("")
def read_words():
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
return words
def read_password():
# read() is the whole file, as one string.
with open("password.txt", "r") as password_file:
password = password_file.read().strip()
return password
def read_log():
# Each line of the log is a guess and how many letters it got right,
# with a comma between: crash,2
guesses = []
with open("log.txt", "r") as log_file:
for line in log_file:
guesses.append(line.strip())
return guesses
# How many letters of the guess are the right letter in the right place.
# crash and crack share c, r, a in the same places, so that is 3.
def letters_right(guess, password):
right = 0
for i in range(len(password)):
if guess[i] == password[i]:
right = right + 1
return right
tries_left = TRIES
playing = True
over = False
@scene("server")
def server(number):
global tries_left, playing, over
# A new server: its own files, written and then read. The password is
# one letter longer every two servers.
length = LENGTH_START + (number - 1) // 2
if length > LENGTH_MOST:
length = LENGTH_MOST
how_many = WORDS_START + (number - 1) * WORDS_MORE
if how_many > WORDS_MOST:
how_many = WORDS_MOST
write_files(length, how_many)
words = read_words()
tries_left = TRIES
playing = True
over = False
# ------------------------------------------------------- the screen
add([text("SERVER " + str(number), size=28), pos(20, 20), color(GREEN)])
tries_label = add([text("TRIES LEFT " + str(TRIES), size=24),
pos(780, 24), anchor("topright"), color(GREEN)])
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
add([text("log.txt", size=20), pos(330, 100), color(DIM)])
# The words, read from words.txt. Each one is something to click:
# area() lets it know when the mouse is over it, and the dictionary
# gives it two values of its own -- the word, and whether it has been
# tried yet.
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN), area(),
{"word": word, "tried": False}, "word"])
y = y + 32
message = add([text("", size=36), pos(560, 450), anchor("center"),
color(GREEN), z(10)])
under = add([text("", size=22), pos(560, 495), anchor("center"),
color(GREEN), z(10)])
# ------------------------------------------------------------ the log
#
# Read log.txt and put every line of it on the screen. The old lines
# are taken away first, so each guess is shown once.
def show_log():
destroyAll("entry")
y = 130
for guess in read_log():
# "crash,2" becomes ["crash", "2"]: split(",") cuts it up at
# the comma.
parts = guess.split(",")
add([text("> " + parts[0], size=24), pos(340, y), color(GREEN),
"entry"])
add([text(parts[1] + "/" + str(length) + " right", size=24),
pos(500, y), color(DIM), "entry"])
y = y + 32
# ------------------------------------------------------------ guessing
# A word lights up while the mouse is over it, unless it has been tried.
@onUpdate("word")
def light_up(row):
if row.tried:
row.color = DIM
elif row.isHovering() and playing:
row.color = BRIGHT
else:
row.color = GREEN
# onClick runs on every click, wherever it is, and a tap on a phone
# counts as a click. isHovering() asks whether the mouse is over a word,
# which needs area() on it.
@onClick
def click():
if not playing:
return
for row in get("word"):
if row.isHovering() and not row.tried:
guess(row)
return
def guess(row):
global tries_left
row.tried = True
password = read_password()
right = letters_right(row.word, password)
# The guess goes on the end of the log. "a" means add: it keeps
# what is there and writes after it, where "w" would empty it.
with open("log.txt", "a") as log_file:
log_file.write(row.word + "," + str(right) + "\n")
show_log()
if row.word == password:
granted(password)
return
tries_left = tries_left - 1
tries_label.text = "TRIES LEFT " + str(tries_left)
shake(4)
if tries_left == 0:
locked_out(password)
# --------------------------------------------------- in, or locked out
def granted(password):
global playing
playing = False
message.text = "ACCESS GRANTED"
under.text = "the password was " + password
wait(2, next_server)
def next_server():
go("server", number + 1)
def locked_out(password):
global playing
playing = False
shake(12)
message.text = "LOCKED OUT"
message.color = RED
under.text = "it was " + password + " -- servers hacked: " + str(number - 1)
# A second before you can start again, so a click meant for a word
# does not skip past what happened.
wait(1, ready_again)
def ready_again():
global over
over = True
add([text("press SPACE or click to start again", size=22),
pos(560, 540), anchor("center"), color(GREEN)])
def again():
if over:
go("server", 1)
onKeyPress("space", again)
onClick(again)
go("server", 1)
WORDShas words of three lengths, all mixed together.write_filesis told the length and picks out only the words that are that long:if len(word) == length. A password and its guesses always have the same number of letters.- The scene is given the server's number:
def server(number), andgo("server", number + 1)goes to the next one.(number - 1) // 2goes up by one every two servers, so servers 1 and 2 have five-letter words, 3 and 4 six, and from 5 on, seven. Every server has one more word than the last, up toWORDS_MOST, because fourteen is all that fit on the screen. - There are 24 words of each length. Ask
write_filesfor more than there are and itswhileloop could never find them: it would go round for ever, and the game would freeze. So it never asks for more thanlen(fits).
Break it. In
write_files, take out if len(word) == length: and
move fits.append(word) back to line up with it, so every word
fits. Now the words on the screen are all different lengths. Guess a word
that is shorter than the password and it stops with
IndexError: string index out of range
on if guess[i] == password[i]:. letters_right
goes along the password, and the guess runs out of letters first.
guess[5] of a five-letter word is a place that isn't
there.
Step 6 — the whole game
Sounds, the best score, and, the first time you play, a message that says what to do. This is the game you played, line for line.
- The best score is the most servers you got into.
setDatasaves it andgetDatareads it back, even after the page is closed. say()puts the instructions up the first time and waits until you close them.toldremembers that it has been said, so it isn't said again every server.
"""Log Hacker -- guess the server's password. Every guess tells you how close.
click a word to try it as the password
You have broken into a server. One of the words on the left is its
password, and you have four tries. Every wrong guess goes in the log on
the right, with how many of its letters are the right letter in the
right place. Use that to rule words out: the password has to match every
line in the log. Get it, and the next server's password is longer.
There is no clock. Take as long as you like.
HOW THIS FILE IS PUT TOGETHER
1. the window and the sounds
2. the words
3. the settings
4. writing the server's files
5. reading them back
6. how close a guess is
7. the game scene: the words, guessing, the log
8. start the game
This is the game for files. A real hacker would find the files already on
the server. This game has to make its own first, and then it reads them
back exactly as it would read any file. There are three:
words.txt the words it could be, one on each line
password.txt the one it is
log.txt every guess so far, and how close it was
Every file is opened with `with`, which closes it again by itself at the
end of the indented lines underneath.
"""
from kaypy import *
# ----------------------------------------------------------------- 1. setup
kaypy(width=800, height=600, background=[8, 14, 10])
loadSound("blip", "sounds/small_beep.wav")
loadSound("granted", "sounds/rising_beep.wav")
loadSound("wrong", "sounds/thump.wav")
loadSound("locked", "sounds/small_boom.wav")
# --------------------------------------------------------------- 2. the words
#
# Every password is one of these. A server only uses words of one length,
# so they are mixed together here and picked out by length later. A lot of
# them look alike -- crash, crack, track -- which is what makes how close a
# guess was worth knowing.
WORDS = ["admin", "login", "guest", "pixel", "cyber", "virus", "token",
"cache", "patch", "debug", "shell", "stack", "crash", "crack",
"track", "trace", "trick", "brick", "block", "clock", "click",
"flash", "stash", "slash",
"server", "binary", "cookie", "kernel", "packet", "router",
"script", "secret", "socket", "pocket", "rocket", "locket",
"system", "matrix", "shadow", "access", "backup", "buffer",
"hacker", "logger", "loader", "locker", "master", "poster",
"account", "browser", "network", "malware", "monitor", "program",
"console", "hacking", "decrypt", "encrypt", "virtual", "digital",
"gateway", "storage", "process", "compile", "install", "payload",
"cracker", "tracker", "packets", "sockets", "rockets", "pockets"]
# -------------------------------------------------------------- 3. settings
#
# Everything else you would want to change is here. Change one at a time
# and play it.
TRIES = 4 # how many guesses you get on each server
WORDS_START = 8 # how many words the first server has
WORDS_MORE = 1 # ...one more on each server after it
WORDS_MOST = 14 # ...and never more than this, or they run off the screen
LENGTH_START = 5 # how long the first server's password is
LENGTH_MOST = 7 # every two servers it is one longer, up to this
GREEN = rgb(90, 230, 120)
DIM = rgb(50, 110, 65)
BRIGHT = rgb(200, 255, 210)
# The name the best score is saved under. Every game on a website shares
# the same storage, so the game's own name goes in it.
BEST_KEY = "log_hacker_best"
# --------------------------------------------- 4. writing the server's files
#
# with open("words.txt", "w") as words_file: opens the file, and calls it
# words_file for the indented lines underneath. When they end, the file is
# closed by itself. "w" means write: it makes the file, or empties it if
# it is already there.
#
# "\n" is the end of a line. write() does not add one for you, so without
# it every word would end up on the same line: crashcracktrack.
def write_files(length, how_many):
# Every word in WORDS that is the right length.
fits = []
for word in WORDS:
if len(word) == length:
fits.append(word)
# The while loop below picks different words until it has enough. Ask
# it for more than there are and it could never finish: it would go
# round for ever, and the game would freeze. So never ask for more.
if how_many > len(fits):
how_many = len(fits)
words = []
while len(words) < how_many:
word = choose(fits)
if word not in words:
words.append(word)
with open("words.txt", "w") as words_file:
for word in words:
words_file.write(word + "\n")
# One of them is the password.
with open("password.txt", "w") as password_file:
password_file.write(choose(words) + "\n")
# A new log, with nothing in it yet. "w" empties it.
with open("log.txt", "w") as log_file:
log_file.write("")
# --------------------------------------------------- 5. reading them back
#
# "r" means read. A for loop over a file goes round once for each line,
# and line is the line, "\n" and all. strip() takes the "\n" off --
# without it, "crash\n" is not "crash", and no guess would ever be right.
def read_words():
words = []
with open("words.txt", "r") as words_file:
for line in words_file:
words.append(line.strip())
return words
def read_password():
# read() is the whole file, as one string.
with open("password.txt", "r") as password_file:
password = password_file.read().strip()
return password
def read_log():
# Each line of the log is a guess and how many letters it got right,
# with a comma between: crash,2
guesses = []
with open("log.txt", "r") as log_file:
for line in log_file:
guesses.append(line.strip())
return guesses
# ------------------------------------------------- 6. how close a guess is
#
# How many letters of the guess are the right letter in the right place.
# crash and crack share c, r, a in the same places, so that is 3.
def letters_right(guess, password):
right = 0
for i in range(len(password)):
if guess[i] == password[i]:
right = right + 1
return right
# ------------------------------------------------------- 7. the game scene
#
# These change while you play. A function that CHANGES one has to say
# `global` first, which means "the one up here, not a new one of my own".
tries_left = TRIES
playing = True
over = False
told = False
@scene("server")
def server(number):
global tries_left, playing, over, told
# A new server: its own files, written and then read. The password is
# one letter longer every two servers.
length = LENGTH_START + (number - 1) // 2
if length > LENGTH_MOST:
length = LENGTH_MOST
how_many = WORDS_START + (number - 1) * WORDS_MORE
if how_many > WORDS_MOST:
how_many = WORDS_MOST
write_files(length, how_many)
words = read_words()
tries_left = TRIES
playing = True
over = False
best = getData(BEST_KEY, 0)
# ------------------------------------------------------- the screen
add([text("SERVER " + str(number), size=28), pos(20, 20), color(GREEN)])
best_label = add([text("best " + str(best), size=18), pos(20, 52),
color(DIM)])
tries_label = add([text("TRIES LEFT " + str(TRIES), size=24),
pos(780, 24), anchor("topright"), color(GREEN)])
add([text("words.txt", size=20), pos(40, 100), color(DIM)])
add([text("log.txt", size=20), pos(330, 100), color(DIM)])
# The words, read from words.txt. Each one is something to click:
# area() lets it know when the mouse is over it, and the dictionary
# gives it two values of its own -- the word, and whether it has been
# tried yet.
y = 130
for word in words:
add([text(word, size=26), pos(50, y), color(GREEN), area(),
{"word": word, "tried": False}, "word"])
y = y + 32
message = add([text("", size=36), pos(560, 450), anchor("center"),
color(GREEN), z(10)])
under = add([text("", size=22), pos(560, 495), anchor("center"),
color(GREEN), z(10)])
# ------------------------------------------------------------ the log
#
# Read log.txt and put every line of it on the screen. The old lines
# are taken away first, so each guess is shown once.
def show_log():
destroyAll("entry")
y = 130
for guess in read_log():
# "crash,2" becomes ["crash", "2"]: split(",") cuts it up at
# the comma.
parts = guess.split(",")
add([text("> " + parts[0], size=24), pos(340, y), color(GREEN),
"entry"])
add([text(parts[1] + "/" + str(length) + " right", size=24),
pos(500, y), color(DIM), "entry"])
y = y + 32
# ------------------------------------------------------------ guessing
# A word lights up while the mouse is over it, unless it has been tried.
@onUpdate("word")
def light_up(row):
if row.tried:
row.color = DIM
elif row.isHovering() and playing:
row.color = BRIGHT
else:
row.color = GREEN
# onClick runs on every click, wherever it is, and a tap on a phone
# counts as a click. isHovering() asks whether the mouse is over a word,
# which needs area() on it.
@onClick
def click():
if not playing:
return
for row in get("word"):
if row.isHovering() and not row.tried:
guess(row)
return
def guess(row):
global tries_left
row.tried = True
password = read_password()
right = letters_right(row.word, password)
# The guess goes on the end of the log. "a" means add: it keeps
# what is there and writes after it, where "w" would empty it.
with open("log.txt", "a") as log_file:
log_file.write(row.word + "," + str(right) + "\n")
show_log()
if row.word == password:
granted(password)
return
tries_left = tries_left - 1
tries_label.text = "TRIES LEFT " + str(tries_left)
play("wrong")
shake(4)
if tries_left == 0:
locked_out(password)
# --------------------------------------------------- in, or locked out
def granted(password):
global playing
playing = False
play("granted")
message.text = "ACCESS GRANTED"
under.text = "the password was " + password
wait(2, next_server)
def next_server():
go("server", number + 1)
def locked_out(password):
global playing
playing = False
play("locked")
shake(12)
# The servers you got into is one less than the one you were
# locked out of.
hacked = number - 1
if hacked > best:
setData(BEST_KEY, hacked)
best_label.text = "best " + str(hacked)
message.text = "LOCKED OUT"
message.color = RED
under.text = "it was " + password + " -- servers hacked: " + str(hacked)
# A second before you can start again, so a click meant for a word
# does not skip past what happened.
wait(1, ready_again)
def ready_again():
global over
over = True
add([text("press SPACE or click to start again", size=22),
pos(560, 540), anchor("center"), color(GREEN)])
def again():
if over:
go("server", 1)
onKeyPress("space", again)
onClick(again)
# The first time, how to play comes up first.
if not told:
told = True
say("One of these words is the server's password, and you have "
"four tries. Click a word to guess it. The log tells you how "
"many of its letters are in the right place -- the password "
"has to match every line of it. There is no clock.")
# ------------------------------------------------------- 8. start the game
go("server", 1)
Make it yours
Open the whole game in the playground → It goes straight into the editor, ready to change and run.
- Your own words. Put your own in
WORDS. Words that look alike make the best game:pocket,rocket,socketandlocketare all one letter apart. - Easier, or harder.
TRIES = 6to start with.TRIES = 3once you're good at it. - A record of every game. In
granted, open"history.txt"with"a"and write the password on the end. Nothing ever empties it, so it grows by one line every server you get into. Read it back and show how many lines it has. - A hint file. Write a
hints.txtwith a line for each word, likebrick,a building block, read it back withsplit(","), and show the hint for the password at the start.