Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
da5a1a0
Initial games portal
github-actions[bot] Sep 21, 2025
a04efa6
feat: Open Research Games Portal
github-actions[bot] Sep 21, 2025
5a49cae
directory check
github-actions[bot] Sep 21, 2025
1fefa14
docs: Add comprehensive section comments to all GitHub workflows and …
github-actions[bot] Sep 21, 2025
3e1ec82
duplicates handling
github-actions[bot] Sep 21, 2025
78923c9
Merge remote-tracking branch 'origin/expcomments' into orgp
github-actions[bot] Sep 21, 2025
526743b
feat: Complete Open Research Games Portal with working URLs and dupli…
github-actions[bot] Sep 21, 2025
fae6832
Merge branch 'master' into orgp
LukasWallrich Sep 23, 2025
982101e
Merge branch 'master' into orgp
LukasWallrich Sep 23, 2025
b46f832
Merge branch 'master' into orgp
richarddushime Sep 27, 2025
5268e79
Merge branch 'master' into orgp
richarddushime Sep 27, 2025
9ccd71c
Merge branch 'master' into orgp
richarddushime Oct 22, 2025
ca535dc
open-research-games contents update
richarddushime Oct 22, 2025
32faef2
url updates
richarddushime Oct 22, 2025
c6fea47
re.deploy
richarddushime Oct 23, 2025
66bbe9a
Merge branch 'master' into orgp
richarddushime Oct 27, 2025
1d494c9
Drop Game cards and link to portal
richarddushime Oct 29, 2025
6e2804c
Merge branch 'master' into orgp
richarddushime Oct 29, 2025
8d7f47b
Merge branch 'master' into orgp
richarddushime Oct 29, 2025
3c61e88
url fix
richarddushime Oct 29, 2025
ba3f0a9
Merge branch 'master' into orgp
richarddushime Oct 29, 2025
1e177ed
feat: Open Research Games Portal
github-actions[bot] Sep 21, 2025
6acb778
directory check
github-actions[bot] Sep 21, 2025
e9aeaf7
embed app in forrt
richarddushime Oct 29, 2025
5451100
review with Iris
richarddushime Oct 30, 2025
7b7d70f
Merge branch 'master' into orgp
richarddushime Oct 30, 2025
d1b9934
scripts update
richarddushime Oct 30, 2025
c5ca02b
rm open research games data from CI
richarddushime Oct 31, 2025
f93f771
cleanup of old css
richarddushime Oct 31, 2025
431afb1
Merge branch 'master' into orgp
richarddushime Nov 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions .github/workflows/data-processing.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
name: Data Processing

# This workflow is triggered daily at midnight and can also be manually triggered.
# Contributor analysis and GA PR creation only run on the 1st of the month.
# It processes data from various scripts and uploads the processed data as an artifact.
# The data is used to update the website's content.
# FORRT Data Processing Workflow
#
# Purpose: Automated data fetching and processing for FORRT website content
#
# Triggers:
# - Weekly on Sundays at midnight UTC (scheduled)
# - Manual trigger via GitHub Actions UI (workflow_dispatch)
#
# Data Sources Processed:
# 1. Curated Resources (Python script)
# 3. Google Analytics data (Python script)
# 4. Contributor analysis (R script) - Monthly only
#
# Outputs:
# - Updated JSON data files in data/ directory
# - Static copies in static/data/ for client-side access
# - Automated PRs for contributor analysis (monthly)
#
# The processed data is used throughout the Hugo website for dynamic content.

on:
schedule:
- cron: '0 0 * * *' # Daily at Midnight
- cron: '0 0 * * 0' # Weekly on Sundays at Midnight UTC
workflow_dispatch:
inputs:
regenerate_glossary:
Expand All @@ -26,6 +41,7 @@ jobs:
env:
PYTHON_VERSION: "3.11"
steps:

#================
# Repository Setup
#================
Expand Down Expand Up @@ -180,9 +196,6 @@ jobs:
run: python3 content/glossary/_create_glossaries.py
# Execute the glossary script that generates glossary markdown files

#====================
# Google Analytics Data
#====================
#========================================
# Download Google Analytics data and validate
#========================================
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ jobs:

# =======================
# Deployment Artifact
# =======================
#========================================
# Upload built website as artifact for deployment
#========================================
Expand Down
2 changes: 1 addition & 1 deletion assets/scss/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
display: block !important;
opacity: 1 !important;
}
}
}
37 changes: 22 additions & 15 deletions config/_default/menus.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,94 +173,101 @@
weight = 35
parent = "nexus"

[[main]]
name = "Open Research Games Portal"
url = "/games"
# url = "https://forrt.org/apps/open_research_games_portal.html"
weight = 36
parent = "nexus"

[[main]]
name = "Developing Countries & OS"
url = "/os-developing-world"
weight = 36
weight = 37
parent = "nexus"

[[main]]
name = "Educators' Corner"
url = "/educators-corner"
weight = 37
weight = 38
parent = "nexus"

[[main]]
name = "Equity in Open Science"
url = "/equityinos"
weight = 38
weight = 39
parent = "nexus"

[[main]]
name = "Glossary"
url = "/glossary"
weight = 39
weight = 40
parent = "nexus"

[[main]]
name = "Impact of OS on students"
url = "/impact"
weight = 40
weight = 41
parent = "nexus"

[[main]]
name = "Lesson Plans"
url = "/lesson-plans/"
weight = 41
weight = 42
parent = "nexus"

[[main]]
name = "Mapping OS Communities"
url = "/mapping_os"
weight = 42
weight = 43
parent = "nexus"

[[main]]
name = "Neurodiversity Team"
url = "/neurodiversity"
weight = 43
weight = 44
parent = "nexus"

[[main]]
name = "Pedagogies"
url = "/pedagogies"
weight = 44
weight = 45
parent = "nexus"

[[main]]
name = "Self-Assessment"
url = "/self-assessment"
weight = 45
weight = 46
parent = "nexus"

[[main]]
name = "Social Justice Initiatives"
url = "/dei"
weight = 46
weight = 47
parent = "nexus"

[[main]]
name = "Summaries"
url = "/summaries"
weight = 47
weight = 48
parent = "nexus"

[[main]]
name = "Syllabi"
url = "/syllabus"
weight = 48
weight = 49
parent = "nexus"

[[main]]
name = "Teaching OS"
url = "/teaching_os"
weight = 49
weight = 50
parent = "nexus"

[[main]]
name = "Wheel of Privilege"
url = "/awop"
weight = 50
weight = 51
parent = "nexus"


Expand Down
30 changes: 30 additions & 0 deletions content/games/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: Open Research Games Portal
type: plain_page
---
FORRT is excited to launch the Open Research Games Portal – a crowdsourced, pedagogically informed database of games and interactive activities for teaching open and reproducible research practices. This initiative recognizes the power of game-based learning to make complex topics more accessible, memorable, and engaging for learners – from students and early-career researchers to educators and professionals. Whether you're looking for a lighthearted icebreaker or a serious, learning-focused game to integrate into your curriculum, the Portal helps you find what you need.

<a href="https://forrtapps.shinyapps.io/open-research-games-portal/" class="btn btn-primary btn-lg btn-block" style="line-height:1;border-radius:6px; font-size:1.5rem; ">
Open Research Games Portal
</a>

<br>

## What’s in the Portal

We gather extensive information on each game – from metadata and gameplay characteristics to user testimonials, formal evaluations (when available), preparation requirements, and pedagogical suggestions for teaching. This depth is made possible through crowdsourcing: anyone can add missing information and share their experiences with a game, which helps everyone navigate the growing collection of open research games more easily. The Portal serves as an open-access resource offering both digital and physical games that support learning through play, collaboration, and critical thinking.

## Navigate the Portal

You can search and filter games using topic tags, FORRT Clusters, gameplay styles (competitive, collaborative, etc.), and other criteria to find exactly what you're looking for. And importantly, we show you where to find and access these games.


## How to contribute

Use our [Additions Form](https://forms.gle/MSBWR87GchDo8fED7) to add information about games already in the Portal, or the [New Entries Form](https://forms.gle/PXYBrRhXGiZyi8M99) to add games we're missing. You can add any game you know of, even if you haven't played it yourself – the community will fill in the rest!

You can find the Open Research Games Portal [here](https://forrtapps.shinyapps.io/open-research-games-portal/)

We're continuing to improve the Portal and would love your feedback on both the database and our forms. Please reach out to [games@forrt.com](games@forrt.com) with any comments or suggestions.

---
170 changes: 170 additions & 0 deletions scripts/open_research_games_portal/Open-Research-Games-Portal.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Libraries
library(readxl)
library(dplyr)
library(tidyr)
library(googlesheets4)
library(stringr)
library(jsonlite)

# Disable authentication for public sheets
gs4_deauth()

# Google Sheets URLs
google_sheet_url <- "https://docs.google.com/spreadsheets/d/1cmydWjD1OuyKxJVfDlv0N3T474zwymfB04yFDZQO-TY/edit?usp=sharing"
google_sheet_csv_url <- "https://docs.google.com/spreadsheets/d/e/2PACX-1vRxW5RjnjrJ7KtLo3o8yRjXS8fr3bKOyOwUE_k1b8cN2LRpwkCY3i6Cgo7dZBVFQuyfVywEymMlXRTM/pub?output=csv"

# Try different methods to read the data
df_combined <- NULL

# CSV export (most reliable)
cat("Attempting to read from CSV export (TEST sheet)...\n")
tryCatch({
# TEST sheet
test_csv_url <- "https://docs.google.com/spreadsheets/d/e/2PACX-1vRxW5RjnjrJ7KtLo3o8yRjXS8fr3bKOyOwUE_k1b8cN2LRpwkCY3i6Cgo7dZBVFQuyfVywEymMlXRTM/pub?gid=610093275&single=true&output=csv"
df_combined <- read.csv(test_csv_url, stringsAsFactors = FALSE)
cat("Successfully read", nrow(df_combined), "rows and", ncol(df_combined), "columns from CSV export (TEST sheet)\n")
cat("Columns read:", names(df_combined), "\n")
}, error = function(e) {
cat("CSV export failed:", e$message, "\n")
})

# Google Sheets API without authentication (if CSV failed)
if (is.null(df_combined)) {
cat("Attempting to read from Google Sheets API without authentication (TEST sheet)...\n")
tryCatch({
df_combined <- read_sheet(google_sheet_url, sheet = "TEST")
cat("Successfully read", nrow(df_combined), "rows and", ncol(df_combined), "columns from Google Sheets API (TEST sheet)\n")
cat("Columns read:", names(df_combined), "\n")
}, error = function(e) {
cat("Google Sheets API also failed:", e$message, "\n")
})
}

# If both failed, stop with helpful message
if (is.null(df_combined)) {
cat("\n")
cat(paste(rep("=", 50), collapse = ""))
cat("\n")
cat("ERROR: Unable to read data from Google Sheets\n")
cat(paste(rep("=", 50), collapse = ""))
cat("\n")
cat("Solutions:\n")
cat("1. Make the Google Sheet public:\n")
cat(" - Open the sheet\n")
cat(" - Click Share > Anyone with the link > Viewer\n")
cat("2. Or download as CSV and use local file\n")
cat("3. Or set up proper Google Sheets API authentication\n")
stop("Please fix Google Sheets access and try again.")
}

# Standardize column names
names(df_combined) <- tolower(gsub("[^A-Za-z0-9_]", "_", names(df_combined)))
names(df_combined) <- gsub("_+", "_", names(df_combined))
names(df_combined) <- gsub("^_|_$", "", names(df_combined))

# Clean and prepare the data
Portal <- df_combined
if ("game_id" %in% names(Portal)) {
Portal <- Portal[order(Portal$game_id), ] # Order by Game ID
} else if ("unique_id" %in% names(Portal)) {
Portal <- Portal[order(Portal$unique_id), ] # Order by Unique ID
}

# Function to prepare data for JSON output
prepare_json_data <- function(df) {
df %>%
# Clean column names for JSON compatibility
rename_with(~ gsub("[^A-Za-z0-9_]", "_", .x)) %>%
rename_with(~ gsub("_+", "_", .x)) %>%
rename_with(~ gsub("^_|_$", "", .x)) %>%
rename_with(~ tolower(.x)) %>%
# Convert NA values to empty strings
mutate(across(everything(), ~ ifelse(is.na(.x), "", as.character(.x)))) %>%
# Create a slug for each game based on title or game_id
mutate(
slug = ifelse(
!is.na(game_id) & game_id != "",
gsub("[^A-Za-z0-9]", "-", tolower(paste0(title, "-", game_id))),
gsub("[^A-Za-z0-9]", "-", tolower(title))
)
) %>%
# Clean slug
mutate(slug = gsub("-+", "-", slug)) %>%
mutate(slug = gsub("^-|-$", "", slug)) %>%
# Split all fields except slug by bullet points or newlines into arrays
mutate(
across(
.cols = -slug, # Exclude slug from strsplit
.fns = ~ strsplit(as.character(.x), "\\s*•\\s*|\\r?\\n\\s*")
)
) %>%
# Clean up resulting arrays to remove empty elements
mutate(
across(
.cols = -slug, # Exclude slug from cleaning
.fns = ~ lapply(.x, function(x) x[x != "" & !is.na(x)])
)
)
}

# Create JSON data
create_json_data_file <- function(df, output_file) {
# Define all expected columns
expected_columns <- c(
"game_id", "title", "creators", "description", "access", "delivery_format",
"game_type", "gameplay_style","tone", "number_of_players", "target_audience",
"last_updated", "language", "licence", "topic_area", "forrt_clusters",
"learning_objectives", "formal_evaluation", "suggested_audience",
"prior_knowledge", "playtime", "scalability", "teaching_integration",
"context_specific_elements", "preparation", "testimonials", "entry_id"
)

games_list <- list()

for (i in 1:nrow(df)) {
game <- df[i, ]
game_data <- list()

# Add all expected fields to the game data
for (col in expected_columns) {
if (col %in% names(game)) {
if (is.list(game[[col]])) {
# Handle list fields (arrays)
if (length(game[[col]][[1]]) > 0 && game[[col]][[1]][1] != "") {
game_data[[col]] <- game[[col]][[1]][game[[col]][[1]] != ""]
} else {
game_data[[col]] <- list() # Empty array for empty lists
}
} else {
# Handle regular fields
game_data[[col]] <- ifelse(game[[col]] == "" || is.na(game[[col]]), "", as.character(game[[col]]))
}
} else {
game_data[[col]] <- "" # Set missing columns to empty string
}
}

# Use the slug as a character string
games_list[[as.character(game$slug)]] <- game_data
}

# Write JSON data file
json_content <- jsonlite::toJSON(games_list, pretty = TRUE, auto_unbox = TRUE)
writeLines(json_content, output_file)

cat("Created JSON data file:", output_file, "\n")
}

# Main execution
cat("Open Research Games Portal - Data Processing\n")
cat(paste(rep("=", 50), collapse = ""), "\n")

# Prepare data for JSON output
json_data <- prepare_json_data(Portal)

# Create JSON data file
create_json_data_file(json_data, "data/open_research_games.json")

cat("\nOpen Research Games Portal processing completed!\n")
cat("- JSON data file created at data/open_research_games.json\n")
cat("Total games processed:", nrow(Portal), "\n")
Loading