textgames / play_helper.py
fhudi's picture
add google credentials
544c5af
raw
history blame
22.7 kB
# %%
import os
import time
import pandas as pd
import gradio as gr
import google.auth
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
from textgames import GAME_NAMES, LEVEL_IDS, LEVELS, new_game, preload_game, game_filename
from textgames.islands.islands import Islands
from textgames.sudoku.sudoku import Sudoku
from textgames.crossword_arranger.crossword_arranger import CrosswordArrangerGame
from textgames.ordering_text.ordering_text import OrderingTextGame
# %%
def declare_components():
with gr.Row():
with gr.Column(scale=1):
m = gr.Markdown("Welcome to TextGames!", elem_id="md-greeting")
logout_btn = gr.Button("Logout", link="/logout", variant='huggingface', size='sm', elem_id="btn-logout")
with gr.Column(scale=2):
solved_games_df = gr.DataFrame(headers=[g.split('\t', 1)[0] for g in GAME_NAMES], label="Finished Games",
interactive=False, elem_id="df-solved-games")
game_radio = gr.Radio(GAME_NAMES, label="Game", elem_id="radio-game-name")
level_radio = gr.Radio(LEVELS, label="Level", elem_id="radio-level-name")
new_game_btn = gr.Button("Start Game", elem_id="btn-start-game")
render_toggle = gr.Checkbox(False, visible=False, interactive=False)
return m, logout_btn, solved_games_df, game_radio, level_radio, new_game_btn, render_toggle
# %%
_creds, _ = google.auth.load_credentials_from_dict({
"type": "service_account",
"project_id": os.getenv("GOOGLE_AUTH_CREDS_PROJECT_ID", ""),
"private_key_id": os.getenv("GOOGLE_AUTH_CREDS_PRIVATE_KEY_ID", ""),
"private_key": os.getenv("GOOGLE_AUTH_CREDS_PRIVATE_KEY", ""),
"client_email": os.getenv("GOOGLE_AUTH_CREDS_CLIENT_EMAIL", ""),
"client_id": os.getenv("GOOGLE_AUTH_CREDS_CLIENT_ID", ""),
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": os.getenv("GOOGLE_AUTH_CREDS_CLIENT_X509_CERT_URL", ""),
"universe_domain": "googleapis.com"
})
_service = build("drive", "v3", credentials=_creds)
_files = _service.files()
# %%
js_remove_input_helper = """(s) => {
var el = document.getElementById('lintao-container');
if (el) el.remove();
return s;
}"""
# %%
js_solved_games_df_and_remove_footers = """() => {
var solvedGamesDf = document.getElementById("df-solved-games");
var tables = solvedGamesDf.getElementsByTagName("table");
for (let i = 0; i < tables.length; ++i) {
tables[i].style.overflowY = "clip";
tables[i].style.overflowX = "auto";
}
var footers = document.getElementsByTagName("footer");
for (let i = 0; i < footers.length; ++i) {
// footers[i].style.visibility = 'hidden';
footers[i].remove();
}
}"""
# %%
js_island = """
function island() {{
const grid_N = {N},
grid_px = 40;
const container = document.createElement('div');
container.style.display = 'grid';
container.style.gridTemplateColumns = container.style.gridTemplateRows = `repeat(${{grid_N}}, ${{grid_px}}px)`;
container.style.gap = '1px';
container.style.border = '2px solid black';
container.style.width = 'max-content';
container.style.margin = '5px 0px 5px 40px';
container.id = 'lintao-container';
for (let i = 0; i < grid_N; ++i) {{
for (let j = 0; j < grid_N; ++j) {{
const cell = document.createElement('div');
cell.textContent = '.';
cell.style.width = cell.style.height = `${{grid_px}}px`;
cell.style.display = 'flex';
cell.style.alignItems = 'center';
cell.style.justifyContent = 'center';
cell.style.fontSize = `${{grid_px/2}}px`;
cell.style.border = '1px solid gray';
cell.style.cursor = 'pointer';
cell.id = `lintao-cell-${{i}}-${{j}}`;
// Toggle between '#', 'o', and '.'
cell.addEventListener('click', () => {{
if (cell.textContent === '.') {{
cell.textContent = '#';
}} else if (cell.textContent === '#') {{
cell.textContent = 'o';
}} else if (cell.textContent === 'o') {{
cell.textContent = '.';
}} else {{
alert(`The clicked cell has unknown value of '${{cell.textContent}}'.`)
}}
}});
container.appendChild(cell);
}}
}}
// return container;
// var gradioContainer = document.querySelector('.gradio-container');
// gradioContainer.insertBefore(container, gradioContainer.firstChild);
var submitRow = document.getElementById("lintao-submit-row");
submitRow.parentElement.insertBefore(container, submitRow);
}}
"""
js_island_submit = """
function island_submit(textarea, io_history) {{
const grid_N = {N};
var ret = "";
for (let i = 0; i < grid_N; ++i) {{
if (i > 0) ret += '\\n';
for (let j = 0; j < grid_N; ++j) {{
ret += document.getElementById(`lintao-cell-${{i}}-${{j}}`).textContent;
}}
}}
return [ret, io_history];
}}
"""
# %%
js_sudoku = """
function sudoku() {{
const N = {N};
const grid_N = N*N,
grid_px = 50,
border_px = 3;
const mat = {mat};
let is_numeric_sudoku = false;
for (let i = 0; i < grid_N; ++i) {{
for (let j = 0; j < grid_N; ++j) {{
if (/^\\d$/.test(mat[i][j])) {{
is_numeric_sudoku = true;
break;
}}
}}
}}
const container = document.createElement('div');
container.style.display = 'grid';
container.style.gridTemplateColumns = container.style.gridTemplateRows = `repeat(${{grid_N}}, ${{grid_px}}px)`;
container.style.gap = '1px';
container.style.border = '${{border_px}}px solid white';
container.style.width = 'max-content';
container.style.margin = '5px 0px 5px 40px';
container.id = 'lintao-container';
// Generate the grid
for (let i = 0; i < grid_N; ++i) {{
for (let j = 0; j < grid_N; ++j) {{
const cell = document.createElement('input');
cell.type = 'text';
cell.maxLength = 1;
cell.style.width = cell.style.height = `${{grid_px}}px`;
cell.style.display = 'flex';
cell.style.alignItems = 'center';
cell.style.justifyContent = 'center';
cell.style.textAlign = 'center';
cell.style.fontSize = `${{grid_px/2}}px`;
cell.style.border = '1px solid #c0c0c0';
cell.style.backgroundColor = 'black'
cell.style.cursor = 'pointer';
cell.id = `lintao-cell-${{i}}-${{j}}`;
if (mat[i][j] != '_') {{
cell.value = mat[i][j];
cell.style.color = '#D0D0D0'
cell.disabled = true;
}}
//cell.style.color = 'black';
//cell.style.outline = 'none';
if (j % N === 0) cell.style.borderLeft = `${{border_px}}px solid white`;
if (j % N === (N-1)) cell.style.borderRight = `${{border_px}}px solid white`;
if (i % N === 0) cell.style.borderTop = `${{border_px}}px solid white`;
if (i % N === (N-1)) cell.style.borderBottom = `${{border_px}}px solid white`;
// Allow only numbers 1-9 or A-I
cell.addEventListener('input', (e) => {{
if ((N === 2 && (!(is_numeric_sudoku?/^[1-4]$/:/^[A-Da-d]$/).test(e.target.value))) ||
(N === 3 && (!(is_numeric_sudoku?/^[1-9]$/:/^[A-Ia-i]$/).test(e.target.value)))) {{
e.target.value = '';
}}
e.target.value = e.target.value.toUpperCase();
}});
container.appendChild(cell);
}}
}}
container.addEventListener('focusin', (e) => {{
const index = Array.from(container.children).indexOf(e.target);
if (index === -1) return;
const row = Math.floor(index / grid_N);
const col = index % grid_N;
for (let i = 0; i < grid_N * grid_N; ++i) {{
const cell = container.children[i];
const currentRow = Math.floor(i / grid_N);
const currentCol = i % grid_N;
if (currentRow === row || currentCol === col || (Math.floor(currentRow / N) === Math.floor(row / N) && Math.floor(currentCol / N) === Math.floor(col / N))) {{
cell.style.backgroundColor = '#303039';
}} else {{
cell.style.backgroundColor = 'black';
}}
}}
}});
container.addEventListener('focusout', () => {{
for (let i = 0; i < grid_N * grid_N; i++) {{
container.children[i].style.backgroundColor = 'black';
}}
}});
var submitRow = document.getElementById("lintao-submit-row");
submitRow.parentElement.insertBefore(container, submitRow);
}}
"""
js_sudoku_submit = """
function sudoku_submit(textarea, io_history) {{
const N = {N};
const grid_N = N*N;
var ret = "";
for (let i = 0; i < grid_N; ++i) {{
if (i > 0) ret += '\\n';
for (let j = 0; j < grid_N; ++j) {{
ret += document.getElementById(`lintao-cell-${{i}}-${{j}}`).value;
}}
}}
return [ret, io_history];
}}
"""
# %%
js_crossword = """
function crossword() {{
const grid_N = {N},
grid_px = 50;
const container = document.createElement('div');
container.style.display = 'grid';
container.style.gridTemplateColumns = container.style.gridTemplateRows = `repeat(${{grid_N}}, ${{grid_px}}px)`;
container.style.gap = '1px';
container.style.border = '2px solid white';
container.style.width = 'max-content';
container.style.margin = '5px 0px 5px 40px';
container.id = 'lintao-container';
// Generate the grid
for (let i = 0; i < grid_N; ++i) {{
for (let j = 0; j < grid_N; ++j) {{
const cell = document.createElement('input');
//cell.textContent = '';
cell.type = 'text';
cell.maxLength = 1;
cell.style.width = cell.style.height = `${{grid_px}}px`;
cell.style.display = 'flex';
cell.style.alignItems = 'center';
cell.style.justifyContent = 'center';
cell.style.textAlign = 'center';
cell.style.fontSize = `${{grid_px/2}}px`;
cell.style.border = '1px solid #c0c0c0';
cell.style.backgroundColor = 'black'
cell.style.cursor = 'pointer';
cell.id = `lintao-cell-${{i}}-${{j}}`;
// Allow only a-z
cell.addEventListener('input', (e) => {{
if (!/^[a-z]$/.test(e.target.value)) {{
e.target.value = '';
}}
}});
container.appendChild(cell);
}}
}}
var submitRow = document.getElementById("lintao-submit-row");
submitRow.parentElement.insertBefore(container, submitRow);
}}
"""
js_crossword_submit = """
function crossword_submit(textarea, io_history) {{
const grid_N = {N};
var ret = "";
for (let i = 0; i < grid_N; ++i) {{
if (i > 0) ret += '\\n';
for (let j = 0; j < grid_N; ++j) {{
ret += document.getElementById(`lintao-cell-${{i}}-${{j}}`).value;
}}
}}
return [ret, io_history];
}}
"""
# %%
js_ordering = """
function ordering() {{
const listContainer = document.createElement('ul');
listContainer.style.listStyle = 'none';
listContainer.style.padding = '0';
listContainer.style.width = '20em';
listContainer.style.border = '2px solid white';
listContainer.style.margin = '5px 0px 5px 40px';
listContainer.id = 'lintao-container';
document.body.appendChild(listContainer);
const items = {items};
items.forEach((itemText, index) => {{
const listItem = document.createElement('li');
listItem.textContent = itemText;
listItem.draggable = true;
listItem.style.padding = '10px';
listItem.style.border = '1px solid #c0c0c0';
listItem.style.margin = '3px';
listItem.style.backgroundColor = 'black';
listItem.style.cursor = 'grab';
listItem.id = `lintao-item-${{index}}`;
// Drag and drop events
listItem.addEventListener('dragstart', (e) => {{
const draggedIndex = Array.from(listContainer.children).indexOf(listItem);
e.dataTransfer.setData('text/plain', draggedIndex);
listItem.style.backgroundColor = '#1f1811';
}});
listItem.addEventListener('dragover', (e) => {{
e.preventDefault();
listItem.style.backgroundColor = '#303030';
}});
listItem.addEventListener('dragleave', () => {{
listItem.style.backgroundColor = 'black';
}});
listItem.addEventListener('drop', (e) => {{
e.preventDefault();
const draggedIndex = e.dataTransfer.getData('text/plain');
const draggedItem = listContainer.children[draggedIndex];
const targetIndex = Array.from(listContainer.children).indexOf(listItem);
console.log(draggedIndex, draggedItem, targetIndex);
if (draggedIndex !== targetIndex) {{
listContainer.insertBefore(draggedItem, targetIndex > draggedIndex ? listItem.nextSibling : listItem);
}}
listItem.style.backgroundColor = 'black';
}});
listItem.addEventListener('dragend', () => {{
listItem.style.backgroundColor = 'black';
}});
listContainer.appendChild(listItem);
}});
var submitRow = document.getElementById("lintao-submit-row");
submitRow.parentElement.insertBefore(listContainer, submitRow);
}}
"""
js_ordering_submit = """
function ordering_submit(textarea, io_history) {{
var ret = "";
const container =
document.getElementById("lintao-container").childNodes.forEach(
(c, i) => {{
if (i>0) ret += '\\n';
ret += c.textContent;
}}
)
return [ret, io_history];
}}
"""
# %%
def _calc_time_elapsed(start_time, cur_text, is_solved):
if not is_solved:
return f"Time Elapsed (sec): {time.time() - start_time:8.1f}"
else:
return cur_text
# %%
def _get_file_output(game_name, level_id, fn_prefix):
fd = os.getenv('TEXTGAMES_OUTPUT_DIR', '.')
os.makedirs(fd, exist_ok=True)
return f"{fd}/{fn_prefix}_-_{game_filename(game_name)}_{level_id}.pkl"
# %%
def start_new_game(game_name, level, session_state_component, is_solved_component, solved_games_component,
user=None, show_timer=False, uid=None):
# cur_game_id = GAME_IDS[GAME_NAMES.index(game_name)]
difficulty_level = LEVEL_IDS[LEVELS.index(level)]
# if show_timer:
# elapsed_text = gr.Textbox("N/A", label=f"{game_name}", info=f"{level}", )
# gr.Timer(.3).tick(_calc_time_elapsed, [cur_game_start, elapsed_text, is_solved_component], [elapsed_text])
fp_out = _get_file_output(game_name, difficulty_level, uid)
cur_game = (
new_game(game_name, difficulty_level)
if user is None else
preload_game(game_name, difficulty_level, user)
)
cur_game.attach_stats_output_(fp_out)
cur_game.flush_stats_(user=user)
def add_msg(new_msg, prev_msg):
user_input = '\n'.join(new_msg.split())
solved, val_msg = cur_game.validate(user_input)
response = ("Correct guess" if solved else "Bad guess (Wrong Answer)") + "\n" + val_msg
new_io_history = prev_msg + [f"Guess>\n{new_msg}", "Prompt>\n" + response]
return (
("" if not solved else gr.Textbox("Thank you for playing!", interactive=False)),
new_io_history, "\n\n".join(new_io_history), (1 if solved else 0),
)
gr.Markdown(
"""
> ### ‼️ Do ***NOT*** refresh this page. ‼️<br>
> #### ⚠️ Refreshing the page equals "Give-up 😭" ⚠️
"""
)
showhide_helper_btn = gr.Button("Show Input Helper (disabling manual input)", elem_id="lintao-helper-btn")
io_history = gr.State(["Prompt>\n" + cur_game.get_prompt()])
io_textbox = gr.Textbox("\n\n".join(io_history.value), label="Prompt>", interactive=False)
textarea = gr.Textbox(label="Guess>", lines=5, info=f"(Shift + Enter to submit)")
textarea.submit(add_msg, [textarea, io_history], [textarea, io_history, io_textbox, is_solved_component])
js_submit = "(a,b) => [a,b]"
if any([isinstance(cur_game, cls) for cls in (Islands, Sudoku, CrosswordArrangerGame, OrderingTextGame)]):
if isinstance(cur_game, Islands):
js, js_submit = js_island.format(N=cur_game.N), js_island_submit.format(N=cur_game.N)
elif isinstance(cur_game, Sudoku):
sudoku_arr = str(list(map(lambda r: ''.join(map(str, r)), cur_game.mat)))
js, js_submit = js_sudoku.format(N=cur_game.srn, mat=sudoku_arr), js_sudoku_submit.format(N=cur_game.srn)
elif isinstance(cur_game, CrosswordArrangerGame):
js, js_submit = js_crossword.format(N=cur_game.board_size), js_crossword_submit.format(
N=cur_game.board_size)
elif isinstance(cur_game, OrderingTextGame):
js, js_submit = js_ordering.format(items=f"{cur_game.words}"), js_ordering_submit.format()
else:
raise NotImplementedError(cur_game)
showhide_helper_btn.click(lambda: (gr.update(interactive=False), gr.update(interactive=False)), None,
[textarea, showhide_helper_btn], js=js)
else:
showhide_helper_btn.interactive = showhide_helper_btn.visible = False
with gr.Row(elem_id="lintao-submit-row"):
submit_btn = gr.Button("Submit", elem_id="lintao-submit-btn", variant='primary', scale=3)
give_up_btn = gr.Button("Give-up 😭", variant='stop', scale=1)
finish_btn = gr.Button("🎉🎊 ~ Finish Game ~ 🎊🎉", variant='primary', visible=False, interactive=False)
submit_btn.click(add_msg, [textarea, io_history], [textarea, io_history, io_textbox, is_solved_component],
js=js_submit)
give_up_checkbox = gr.Checkbox(False, visible=False, interactive=False)
give_up_btn.click(
lambda x: x, [give_up_checkbox], [give_up_checkbox],
js="(x) => confirm('🥹 Give-up? 💸')"
)
def _forfeiting(confirmed, _solved_games):
if confirmed:
cur_game.finish_stats_(forfeit=True)
if level in LEVELS[1:4] and level not in _solved_games[game_name]:
_solved_games[game_name].append(level)
return 0, _solved_games
return 1, _solved_games
give_up_checkbox.change(_forfeiting, [give_up_checkbox, solved_games_component],
[session_state_component, solved_games_component])
def game_is_solved(_is_solved, _session_state, _solved_games):
if _is_solved:
if level in LEVELS[1:4] and level not in _solved_games[game_name]:
_solved_games[game_name].append(level)
return (
2,
gr.update(visible=False, interactive=False),
gr.update(visible=False, interactive=False),
gr.update(visible=True, interactive=True),
_solved_games,
)
else:
return (
_session_state, gr.update(), gr.update(), gr.update(), _solved_games
)
def upload_to_drive():
fn = fp_out.rsplit("/", 1)[-1]
file_metadata = {"name": fn, "parents": ["1qStKuVerAQPsXagngfzlNg8PdAR5hupA"]}
media = MediaFileUpload(fp_out)
# print(f"{file_metadata}\n{fn}")
try:
_files.create(body=file_metadata, media_body=media).execute()
except HttpError as error:
print(f"An error occurred: {error}")
is_solved_component.change(
game_is_solved,
[is_solved_component, session_state_component, solved_games_component],
[session_state_component, submit_btn, give_up_btn, finish_btn, solved_games_component],
)
finish_btn.click(
upload_to_drive, None, None,
).then(
lambda: (0, 0), None, [session_state_component, is_solved_component]
)
# %%
def check_to_start_new_game(game_name, level, user=None, uid=None):
print(game_name, level)
if game_name is None or level is None:
raise gr.Error("please choose both Game & Level")
fp = _get_file_output(game_name, LEVEL_IDS[LEVELS.index(level)], uid)
if os.path.exists(fp):
raise gr.Error(f"You have done this game already.<br/>{game_name} - {level}")
if user is None:
gr.Warning("no user, game will be generated randomly")
else:
if not user['email_verified']:
gr.Warning("please verify your email address")
elif user['email_verified'] == "mockuser":
gr.Info("game will load with a mocked-up user")
return 1
# %%
def check_played_game(solved_games, uid):
ret = dict()
for game_name in solved_games.keys():
cur = []
for level, level_id in zip(LEVELS[1:4], LEVEL_IDS[1:4]):
if os.path.exists(_get_file_output(game_name, level_id, uid)):
cur.append(level)
ret[game_name] = cur
return ret
# %%
def session_state_change_fn(_session_state, cnt_return_with_val=2, cnt_negate_with_val=0, cnt_return=1, cnt_negate=0):
# print(f"Session state changed to {_session_state}")
ret = (_session_state not in [1, 2])
def up(positive, positive_reset_value=True):
return (
gr.update(interactive=True, value=None) if positive and positive_reset_value else
gr.update(interactive=True) if positive else gr.update(interactive=False)
)
return ([up(ret, True) for _ in range(cnt_return_with_val)] +
[up(not ret, True) for _ in range(cnt_negate_with_val)] +
[up(ret, False) for _ in range(cnt_return)] +
[up(not ret, False) for _ in range(cnt_negate)] +
[])
# %%
def solved_games_change_fn(solved_games):
def _icon(_):
return _.split('\t', 1)[0]
return pd.DataFrame({
_icon(g): [" ".join(map(_icon, l))]
for g, l in solved_games.items()
})
# %%
# %%