<?php
/*
Plugin Name: Sudoku
Description: A plugin to display a daily Sudoku puzzle on your WordPress site. Daily puzzles are stored in the database so you can review older puzzles.
Version: 2.2.0
Author: Mike Vahldieck
Author URI: http://it-breeze.info
Plugin URI: http://it-breeze.info
License: GPL2
*/
<?php
/*
Plugin Name: Sudoku
Description: A plugin to display a daily Sudoku puzzle on your WordPress site. Daily puzzles are stored in the database so you can review older puzzles.
Version: 2.2.0
Author: Mike Vahldieck
Author URI: http://it-breeze.info
Plugin URI: http://it-breeze.info
License: GPL2
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Create a custom database table on plugin activation.
*/
function daily_sudoku_activate() {
global $wpdb;
$table_name = $wpdb->prefix . 'daily_sudoku';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
puzzle_date date NOT NULL,
puzzle longtext NOT NULL,
solution longtext NOT NULL,
difficulty varchar(10) DEFAULT 'medium',
PRIMARY KEY (id),
UNIQUE KEY puzzle_date (puzzle_date)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
}
register_activation_hook( __FILE__, 'daily_sudoku_activate' );
/**
* Shortcode to display the daily sudoku puzzle.
*/
add_shortcode( 'sudoku', 'daily_sudoku_shortcode' );
function daily_sudoku_shortcode() {
global $wpdb;
$table_name = $wpdb->prefix . 'daily_sudoku';
$today = date( 'Y-m-d' );
// Check if today's puzzle exists in the database.
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE puzzle_date = %s", $today ) );
if ( null === $row ) {
// Generate a new puzzle if one doesn't exist for today.
$difficulty = get_option( 'daily_sudoku_difficulty', 'medium' );
$puzzle_data = daily_sudoku_generate_puzzle( $difficulty );
// Insert the new puzzle into the database as JSON.
$wpdb->insert(
$table_name,
[
'puzzle_date' => $today,
'puzzle' => wp_json_encode( $puzzle_data['puzzle'] ),
'solution' => wp_json_encode( $puzzle_data['solution'] ),
'difficulty' => $difficulty,
],
[
'%s',
'%s',
'%s',
'%s',
]
);
} else {
// Retrieve puzzle and solution from the database.
$puzzle_data = [
'puzzle' => json_decode( $row->puzzle, true ),
'solution' => json_decode( $row->solution, true ),
];
}
$puzzle = $puzzle_data['puzzle'];
$solution = $puzzle_data['solution'];
ob_start();
?>
<div id="sudoku-container">
<div id="sudoku-grid">
<?php foreach ( $puzzle as $row_idx => $row ) : ?>
<div class="sudoku-row">
<?php foreach ( $row as $col_idx => $value ) : ?>
<div class="sudoku-cell">
<input type="text" maxlength="1"
data-solution="<?php echo esc_attr( $solution[$row_idx][$col_idx] ); ?>"
value="<?php echo esc_attr( $value ?: '' ); ?>"
<?php echo $value ? 'readonly' : ''; ?> />
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<div class="sudoku-controls">
<button id="check-answer">Check My Answer</button>
<button id="show-solution">Show Solution</button>
</div>
</div>
<?php
return ob_get_clean();
}
/**
* Real Sudoku puzzle generator using backtracking algorithm
*/
function daily_sudoku_generate_puzzle( $difficulty = 'medium' ) {
// Start with empty grid
$grid = array_fill(0, 9, array_fill(0, 9, 0));
// Fill the grid completely using backtracking
daily_sudoku_solve_grid($grid);
// Store the complete solution
$solution = array_map(function($row) { return array_slice($row, 0); }, $grid);
// Remove numbers based on difficulty to create the puzzle
$cells_to_remove = daily_sudoku_get_cells_to_remove($difficulty);
$puzzle = daily_sudoku_create_puzzle($solution, $cells_to_remove);
return [ 'puzzle' => $puzzle, 'solution' => $solution ];
}
/**
* Solve Sudoku grid using backtracking algorithm
*/
function daily_sudoku_solve_grid(&$grid) {
for ($row = 0; $row < 9; $row++) {
for ($col = 0; $col < 9; $col++) {
if ($grid[$row][$col] == 0) {
$numbers = range(1, 9);
shuffle($numbers); // Randomize to get different solutions each time
foreach ($numbers as $num) {
if (daily_sudoku_is_valid($grid, $row, $col, $num)) {
$grid[$row][$col] = $num;
if (daily_sudoku_solve_grid($grid)) {
return true;
}
$grid[$row][$col] = 0; // Backtrack
}
}
return false;
}
}
}
return true;
}
/**
* Check if placing a number at given position is valid
*/
function daily_sudoku_is_valid($grid, $row, $col, $num) {
// Check row
for ($x = 0; $x < 9; $x++) {
if ($grid[$row][$x] == $num) {
return false;
}
}
// Check column
for ($x = 0; $x < 9; $x++) {
if ($grid[$x][$col] == $num) {
return false;
}
}
// Check 3x3 box
$start_row = $row - $row % 3;
$start_col = $col - $col % 3;
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($grid[$i + $start_row][$j + $start_col] == $num) {
return false;
}
}
}
return true;
}
/**
* Get number of cells to remove based on difficulty
*/
function daily_sudoku_get_cells_to_remove($difficulty) {
switch ($difficulty) {
case 'easy':
return rand(35, 45); // Remove 35-45 numbers (leaving 36-46 clues)
case 'medium':
return rand(45, 55); // Remove 45-55 numbers (leaving 26-36 clues)
case 'hard':
return rand(55, 65); // Remove 55-65 numbers (leaving 16-26 clues)
default:
return rand(45, 55);
}
}
/**
* Create puzzle by removing numbers from solved grid
*/
function daily_sudoku_create_puzzle($solution, $cells_to_remove) {
$puzzle = array_map(function($row) { return array_slice($row, 0); }, $solution);
// Create array of all cell positions
$positions = [];
for ($i = 0; $i < 9; $i++) {
for ($j = 0; $j < 9; $j++) {
$positions[] = [$i, $j];
}
}
// Shuffle positions to remove randomly
shuffle($positions);
// Remove numbers from random positions
for ($i = 0; $i < $cells_to_remove && $i < count($positions); $i++) {
$row = $positions[$i][0];
$col = $positions[$i][1];
$puzzle[$row][$col] = '';
}
return $puzzle;
}
/**
* Enqueue front-end scripts and styles.
*/
add_action( 'wp_enqueue_scripts', 'daily_sudoku_enqueue_assets' );
function daily_sudoku_enqueue_assets() {
wp_enqueue_style( 'daily-sudoku-styles', plugin_dir_url( __FILE__ ) . 'sudoku.css' );
wp_enqueue_script( 'daily-sudoku-scripts', plugin_dir_url( __FILE__ ) . 'sudoku.js', [ 'jquery' ], '2.2.0', true );
}
/**
* Add admin menu pages for settings and puzzle history.
*/
add_action( 'admin_menu', 'daily_sudoku_add_admin_menu' );
function daily_sudoku_add_admin_menu() {
add_options_page( 'Daily Sudoku Settings', 'Daily Sudoku', 'manage_options', 'daily-sudoku-settings', 'daily_sudoku_settings_page' );
// Submenu page to view stored puzzles.
add_submenu_page( 'options-general.php', 'Puzzle History', 'Puzzle History', 'manage_options', 'daily-sudoku-history', 'daily_sudoku_history_page' );
}
/**
* Render the settings page.
*/
function daily_sudoku_settings_page() {
if (isset($_POST['generate_today_puzzle'])) {
daily_sudoku_force_generate_today_puzzle();
echo '<div class="notice notice-success"><p>Today\'s puzzle has been regenerated!</p></div>';
}
?>
<div class="wrap">
<h1>Daily Sudoku Settings</h1>
<form method="post" action="options.php">
<?php
settings_fields( 'daily_sudoku_settings_group' );
do_settings_sections( 'daily-sudoku-settings' );
submit_button();
?>
</form>
<hr>
<h2>Admin Tools</h2>
<form method="post">
<p>Force generate a new puzzle for today (will replace existing puzzle if any):</p>
<input type="submit" name="generate_today_puzzle" class="button button-secondary" value="Generate New Puzzle for Today">
</form>
</div>
<?php
}
/**
* Force generate today's puzzle (for admin use)
*/
function daily_sudoku_force_generate_today_puzzle() {
global $wpdb;
$table_name = $wpdb->prefix . 'daily_sudoku';
$today = date('Y-m-d');
$difficulty = get_option('daily_sudoku_difficulty', 'medium');
// Delete existing puzzle for today
$wpdb->delete($table_name, ['puzzle_date' => $today], ['%s']);
// Generate new puzzle
$puzzle_data = daily_sudoku_generate_puzzle($difficulty);
// Insert new puzzle
$wpdb->insert(
$table_name,
[
'puzzle_date' => $today,
'puzzle' => wp_json_encode($puzzle_data['puzzle']),
'solution' => wp_json_encode($puzzle_data['solution']),
'difficulty' => $difficulty,
],
['%s', '%s', '%s', '%s']
);
}
/**
* Register settings.
*/
add_action( 'admin_init', 'daily_sudoku_register_settings' );
function daily_sudoku_register_settings() {
register_setting( 'daily_sudoku_settings_group', 'daily_sudoku_difficulty' );
add_settings_section( 'daily_sudoku_main_section', 'Main Settings', null, 'daily-sudoku-settings' );
add_settings_field(
'daily_sudoku_difficulty',
'Default Difficulty Level',
'daily_sudoku_difficulty_field_render',
'daily-sudoku-settings',
'daily_sudoku_main_section'
);
}
function daily_sudoku_difficulty_field_render() {
$value = get_option( 'daily_sudoku_difficulty', 'medium' );
?>
<select name="daily_sudoku_difficulty">
<option value="easy" <?php selected( $value, 'easy' ); ?>>Easy (36-46 clues)</option>
<option value="medium" <?php selected( $value, 'medium' ); ?>>Medium (26-36 clues)</option>
<option value="hard" <?php selected( $value, 'hard' ); ?>>Hard (16-26 clues)</option>
</select>
<p class="description">This difficulty will be used for new daily puzzles. Changes take effect the next day.</p>
<?php
}
/**
* Render an admin page to view puzzle history.
*/
function daily_sudoku_history_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'daily_sudoku';
// Handle delete action
if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['puzzle_id'])) {
$puzzle_id = intval($_GET['puzzle_id']);
$wpdb->delete($table_name, ['id' => $puzzle_id], ['%d']);
echo '<div class="notice notice-success"><p>Puzzle deleted successfully!</p></div>';
}
$results = $wpdb->get_results( "SELECT * FROM $table_name ORDER BY puzzle_date DESC LIMIT 50" );
?>
<div class="wrap">
<h1>Daily Sudoku Puzzle History</h1>
<p>Showing the last 50 puzzles.</p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Date</th>
<th>Difficulty</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if ( $results ) : ?>
<?php foreach ( $results as $row ) : ?>
<tr>
<td><?php echo esc_html( $row->puzzle_date ); ?></td>
<td><?php echo esc_html( ucfirst($row->difficulty ?? 'medium') ); ?></td>
<td>
<a href="<?php echo add_query_arg(['action' => 'delete', 'puzzle_id' => $row->id]); ?>"
onclick="return confirm('Are you sure you want to delete this puzzle?')"
class="button button-small">Delete</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="3">No puzzles found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
?>
sudoku.js
JAVASCRIPT
jQuery(document).ready(function($) {
let showSolutionActive = false;
let checkAnswerActive = false;
// Check Answer functionality
$('#check-answer').on('click', function() {
checkAnswerActive = !checkAnswerActive;
if (checkAnswerActive) {
$('#sudoku-grid input').each(function() {
if ($(this).val() !== $(this).data('solution').toString()) {
jQuery(document).ready(function($) {
let showSolutionActive = false;
let checkAnswerActive = false;
// Check Answer functionality
$('#check-answer').on('click', function() {
checkAnswerActive = !checkAnswerActive;
if (checkAnswerActive) {
$('#sudoku-grid input').each(function() {
if ($(this).val() !== $(this).data('solution').toString()) {
$(this).css('background-color', '#f8d7da'); // Red for wrong
} else {
$(this).css('background-color', '#d4edda'); // Green for correct
}
});
$(this).text('Hide Mistakes');
} else {
$('#sudoku-grid input').css('background-color', '');
$(this).text('Check My Answer');
}
});
// Show Solution functionality
$('#show-solution').on('click', function() {
showSolutionActive = !showSolutionActive;
if (showSolutionActive) {
// Store user input before showing the solution
$('#sudoku-grid input').each(function() {
if (!$(this).attr('readonly')) {
$(this).data('user-value', $(this).val());
}
$(this).val($(this).data('solution')).css('background-color', '');
});
$(this).text('Hide Solution');
} else {
// Restore user input when hiding the solution
$('#sudoku-grid input').each(function() {
if (!$(this).attr('readonly')) {
$(this).val($(this).data('user-value') || '').css('background-color', '');
}
});
$(this).text('Show Solution');
}
});
// Auto-focus next input and validate input
$('#sudoku-grid input').on('input', function() {
var value = $(this).val();
// Only allow numbers 1-9
if (!/^[1-9]$/.test(value)) {
$(this).val('');
return;
}
// Auto-focus to next empty cell
var currentCell = $(this).closest('.sudoku-cell');
var allCells = $('#sudoku-grid .sudoku-cell input:not([readonly])');
var currentIndex = allCells.index(this);
if (currentIndex >= 0 && currentIndex < allCells.length - 1) {
allCells.eq(currentIndex + 1).focus();
}
});
// Handle backspace to go to previous cell
$('#sudoku-grid input').on('keydown', function(e) {
if (e.key === 'Backspace' && $(this).val() === '') {
var allCells = $('#sudoku-grid .sudoku-cell input:not([readonly])');
var currentIndex = allCells.index(this);
if (currentIndex > 0) {
allCells.eq(currentIndex - 1).focus();
}
}
});
});
sudoku.css
CSS
/* Container around the Sudoku puzzle */
#sudoku-container {
max-width: 400px;
margin: 20px auto;
text-align: center;
font-family: Arial, sans-serif;
}
/* The overall grid container just stacks .sudoku-row blocks */
#sudoku-grid {
/* Container around the Sudoku puzzle */
#sudoku-container {
max-width: 400px;
margin: 20px auto;
text-align: center;
font-family: Arial, sans-serif;
}
/* The overall grid container just stacks .sudoku-row blocks */
#sudoku-grid {
/* Remove the background-color and gap from previous code */
margin: 0 auto;
}
/* Each row is displayed as a horizontal flex container of 9 cells */
.sudoku-row {
display: flex;
}
/* Base cell style */
.sudoku-cell {
width: 40px;
height: 40px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid #ddd; /* Default thin border around every cell */
}
/* --- Thick borders for the 3x3 blocks --- */
/* 1) Thick top border on the first row */
.sudoku-row:nth-child(1) .sudoku-cell {
border-top: 2px solid #000;
}
/* 2) Thick bottom border on every 3rd row (3, 6, 9) */
.sudoku-row:nth-child(3n) .sudoku-cell {
border-bottom: 2px solid #000;
}
/* 3) Thick left border on the first column in each row */
.sudoku-cell:nth-child(1) {
border-left: 2px solid #000;
}
/* 4) Thick right border on every 3rd column (3, 6, 9) */
.sudoku-cell:nth-child(3n) {
border-right: 2px solid #000;
}
/* Input field styling */
.sudoku-cell input {
width: 100%;
height: 100%;
text-align: center;
font-size: 18px;
border: none;
outline: none;
background: transparent;
}
/* Buttons container */
.sudoku-controls {
margin-top: 10px;
}
/* Buttons styling */
button {
padding: 10px 20px;
font-size: 16px;
margin: 5px;
background-color: #126724;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #005177;
}
This website uses cookies to ensure you get the best experience on our website.